diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e9a7e8d..46f297967 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ Change Log =============================================================================== +Version 2.1.4 *(2017-01-30)* +---------------------------- +* Fixed: Bugs in execution of some scheduled actions (#604, #609) +* Fixed: Multi-currency transactions not exported when format is QIF (#571) +* Fixed: Incorrect date of last export shown in book manager (#615, #617) +* Fixed: Large exports may be reported as successful even if they didn't complete yet (#616) +* Fixed: Custom date range (in reports) does not select correct ending date (#611) +* Fixed: Account color reset when editing an account (#620) +* Fixed: Export to OwnCloud fails if folder already exists +* Fixed: User not notified if export to external target fails +* Improved translations + + Version 2.1.3 *(2016-10-20)* ---------------------------- * Fixed: Scheduled exports execute too often or not at all in some cases diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 48c117f61..b57d1bc49 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -36,5 +36,6 @@ The following (incomplete list of) people (in no particular order) contributed ( * Felipe Morato * Alceu Rodrigues Neto * Salama AB +* Juan Villa Please visit https://crowdin.com/project/gnucash-android for a more complete list of translation contributions \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6b520971a..d72adee5b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,8 @@ apply plugin: 'io.fabric' def versionMajor = 2 def versionMinor = 1 -def versionPatch = 3 -def versionBuild = 1 +def versionPatch = 4 +def versionBuild = 2 def buildTime() { def df = new SimpleDateFormat("yyyyMMdd HH:mm 'UTC'") diff --git a/app/src/main/java/org/gnucash/android/db/adapter/RecurrenceDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/RecurrenceDbAdapter.java index 90ca5dcfe..0783e58b1 100644 --- a/app/src/main/java/org/gnucash/android/db/adapter/RecurrenceDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/RecurrenceDbAdapter.java @@ -20,12 +20,17 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteStatement; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.model.PeriodType; import org.gnucash.android.model.Recurrence; import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; import static org.gnucash.android.db.DatabaseSchema.RecurrenceEntry; @@ -58,7 +63,7 @@ public Recurrence buildModelInstance(@NonNull Cursor cursor) { long multiplier = cursor.getLong(cursor.getColumnIndexOrThrow(RecurrenceEntry.COLUMN_MULTIPLIER)); String periodStart = cursor.getString(cursor.getColumnIndexOrThrow(RecurrenceEntry.COLUMN_PERIOD_START)); String periodEnd = cursor.getString(cursor.getColumnIndexOrThrow(RecurrenceEntry.COLUMN_PERIOD_END)); - String byDay = cursor.getString(cursor.getColumnIndexOrThrow(RecurrenceEntry.COLUMN_BYDAY)); + String byDays = cursor.getString(cursor.getColumnIndexOrThrow(RecurrenceEntry.COLUMN_BYDAY)); PeriodType periodType = PeriodType.valueOf(type); periodType.setMultiplier((int) multiplier); @@ -67,7 +72,7 @@ public Recurrence buildModelInstance(@NonNull Cursor cursor) { recurrence.setPeriodStart(Timestamp.valueOf(periodStart)); if (periodEnd != null) recurrence.setPeriodEnd(Timestamp.valueOf(periodEnd)); - recurrence.setByDay(byDay); + recurrence.setByDays(stringToByDays(byDays)); populateBaseModelAttributes(cursor, recurrence); @@ -79,8 +84,8 @@ public Recurrence buildModelInstance(@NonNull Cursor cursor) { stmt.clearBindings(); stmt.bindLong(1, recurrence.getPeriodType().getMultiplier()); stmt.bindString(2, recurrence.getPeriodType().name()); - if (recurrence.getByDay() != null) - stmt.bindString(3, recurrence.getByDay()); + if (!recurrence.getByDays().isEmpty()) + stmt.bindString(3, byDaysToString(recurrence.getByDays())); //recurrence should always have a start date stmt.bindString(4, recurrence.getPeriodStart().toString()); @@ -90,4 +95,87 @@ public Recurrence buildModelInstance(@NonNull Cursor cursor) { return stmt; } + + /** + * Converts a list of days of week as Calendar constants to an String for + * storing in the database. + * + * @param byDays list of days of week constants from Calendar + * @return String of days of the week or null if {@code byDays} was empty + */ + private static @NonNull String byDaysToString(@NonNull List byDays) { + StringBuilder builder = new StringBuilder(); + for (int day : byDays) { + switch (day) { + case Calendar.MONDAY: + builder.append("MO"); + break; + case Calendar.TUESDAY: + builder.append("TU"); + break; + case Calendar.WEDNESDAY: + builder.append("WE"); + break; + case Calendar.THURSDAY: + builder.append("TH"); + break; + case Calendar.FRIDAY: + builder.append("FR"); + break; + case Calendar.SATURDAY: + builder.append("SA"); + break; + case Calendar.SUNDAY: + builder.append("SU"); + break; + default: + throw new RuntimeException("bad day of week: " + day); + } + builder.append(","); + } + builder.deleteCharAt(builder.length()-1); + return builder.toString(); + } + + /** + * Converts a String with the comma-separated days of the week into a + * list of Calendar constants. + * + * @param byDaysString String with comma-separated days fo the week + * @return list of days of the week as Calendar constants. + */ + private static @NonNull List stringToByDays(@Nullable String byDaysString) { + if (byDaysString == null) + return Collections.emptyList(); + + List byDaysList = new ArrayList<>(); + for (String day : byDaysString.split(",")) { + switch (day) { + case "MO": + byDaysList.add(Calendar.MONDAY); + break; + case "TU": + byDaysList.add(Calendar.TUESDAY); + break; + case "WE": + byDaysList.add(Calendar.WEDNESDAY); + break; + case "TH": + byDaysList.add(Calendar.THURSDAY); + break; + case "FR": + byDaysList.add(Calendar.FRIDAY); + break; + case "SA": + byDaysList.add(Calendar.SATURDAY); + break; + case "SU": + byDaysList.add(Calendar.SUNDAY); + break; + default: + throw new RuntimeException("bad day of week: " + day); + } + } + return byDaysList; + } } diff --git a/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java b/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java index cf7626818..f1c0ffb39 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java +++ b/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java @@ -35,12 +35,10 @@ import com.crashlytics.android.Crashlytics; import com.dropbox.sync.android.DbxAccountManager; -import com.dropbox.sync.android.DbxException; import com.dropbox.sync.android.DbxFile; import com.dropbox.sync.android.DbxFileSystem; import com.dropbox.sync.android.DbxPath; import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.drive.Drive; import com.google.android.gms.drive.DriveApi; import com.google.android.gms.drive.DriveContents; @@ -80,6 +78,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.concurrent.TimeUnit; /** * Asynchronous task for exporting transactions. @@ -142,21 +141,7 @@ protected void onPreExecute() { @Override protected Boolean doInBackground(ExportParams... params) { mExportParams = params[0]; - - switch (mExportParams.getExportFormat()) { - case QIF: - mExporter = new QifExporter(mExportParams, mDb); - break; - - case OFX: - mExporter = new OfxExporter(mExportParams, mDb); - break; - - case XML: - default: - mExporter = new GncXmlExporter(mExportParams, mDb); - break; - } + mExporter = getExporter(); try { // FIXME: detect if there aren't transactions to export and inform the user @@ -179,91 +164,35 @@ public void run() { return false; } - switch (mExportParams.getExportTarget()) { - case SHARING: - List sdCardExportedFiles = moveExportToSDCard(); - shareFiles(sdCardExportedFiles); - return true; - - case DROPBOX: - moveExportToDropbox(); - return true; - - case GOOGLE_DRIVE: - moveExportToGoogleDrive(); - return true; - - case OWNCLOUD: - moveExportToOwnCloud(); - return true; - - case SD_CARD: - moveExportToSDCard(); - return true; + try { + moveToTarget(); + } catch (Exporter.ExporterException e) { + Crashlytics.log(Log.ERROR, TAG, "Error sending exported files to target: " + e.getMessage()); + return false; } - - return false; + return true; } /** * Transmits the exported transactions to the designated location, either SD card or third-party application * Finishes the activity if the export was starting in the context of an activity - * @param exportResult Result of background export execution + * @param exportSuccessful Result of background export execution */ @Override - protected void onPostExecute(Boolean exportResult) { - if (mContext instanceof Activity) { - if (!exportResult) { + protected void onPostExecute(Boolean exportSuccessful) { + if (exportSuccessful) { + if (mContext instanceof Activity) + reportSuccess(); + + if (mExportParams.shouldDeleteTransactionsAfterExport()) { + backupAndDeleteTransactions(); + refreshViews(); + } + } else { + if (mContext instanceof Activity) { Toast.makeText(mContext, mContext.getString(R.string.toast_export_error, mExportParams.getExportFormat().name()), Toast.LENGTH_LONG).show(); - return; - } else { - String targetLocation; - switch (mExportParams.getExportTarget()){ - case SD_CARD: - targetLocation = "SD card"; - break; - case DROPBOX: - targetLocation = "DropBox -> Apps -> GnuCash"; - break; - case GOOGLE_DRIVE: - targetLocation = "Google Drive -> " + mContext.getString(R.string.app_name); - break; - case OWNCLOUD: - targetLocation = mContext.getSharedPreferences( - mContext.getString(R.string.owncloud_pref), - Context.MODE_PRIVATE).getBoolean( - mContext.getString(R.string.owncloud_sync), false) ? - - "ownCloud -> " + - mContext.getSharedPreferences( - mContext.getString(R.string.owncloud_pref), - Context.MODE_PRIVATE).getString( - mContext.getString(R.string.key_owncloud_dir), null) : - "ownCloud sync not enabled"; - break; - default: - targetLocation = mContext.getString(R.string.label_export_target_external_service); - } - Toast.makeText(mContext, - String.format(mContext.getString(R.string.toast_exported_to), targetLocation), - Toast.LENGTH_LONG).show(); - } - } - - if (mExportParams.shouldDeleteTransactionsAfterExport()) { - Log.i(TAG, "Backup and deleting transactions after export"); - backupAndDeleteTransactions(); - - //now refresh the respective views - if (mContext instanceof AccountsActivity){ - AccountsListFragment fragment = ((AccountsActivity) mContext).getCurrentAccountListFragment(); - if (fragment != null) - fragment.refresh(); - } - if (mContext instanceof TransactionsActivity){ - ((TransactionsActivity) mContext).refresh(); } } @@ -274,67 +203,103 @@ protected void onPostExecute(Boolean exportResult) { } } - private void moveExportToGoogleDrive(){ + private Exporter getExporter() { + switch (mExportParams.getExportFormat()) { + case QIF: + return new QifExporter(mExportParams, mDb); + + case OFX: + return new OfxExporter(mExportParams, mDb); + + case XML: + default: + return new GncXmlExporter(mExportParams, mDb); + } + } + + private void moveToTarget() throws Exporter.ExporterException { + switch (mExportParams.getExportTarget()) { + case SHARING: + List sdCardExportedFiles = moveExportToSDCard(); + shareFiles(sdCardExportedFiles); + break; + + case DROPBOX: + moveExportToDropbox(); + break; + + case GOOGLE_DRIVE: + moveExportToGoogleDrive(); + break; + + case OWNCLOUD: + moveExportToOwnCloud(); + break; + + case SD_CARD: + moveExportToSDCard(); + break; + + default: + throw new Exporter.ExporterException(mExportParams, "Invalid target"); + } + } + + private void moveExportToGoogleDrive() throws Exporter.ExporterException { Log.i(TAG, "Moving exported file to Google Drive"); final GoogleApiClient googleApiClient = BackupPreferenceFragment.getGoogleApiClient(GnuCashApplication.getAppContext()); googleApiClient.blockingConnect(); - final ResultCallback fileCallback = new - ResultCallback() { - @Override - public void onResult(DriveFolder.DriveFileResult result) { - if (!result.getStatus().isSuccess()) - Log.e(TAG, "Error while trying to sync to Google Drive"); - else - Log.i(TAG, "Created a file with content: " + result.getDriveFile().getDriveId()); - } - }; - - Drive.DriveApi.newDriveContents(googleApiClient).setResultCallback(new ResultCallback() { - @Override - public void onResult(DriveApi.DriveContentsResult result) { - if (!result.getStatus().isSuccess()) { - Log.e(TAG, "Error while trying to create new file contents"); - return; - } - final DriveContents driveContents = result.getDriveContents(); - try { - // write content to DriveContents - OutputStream outputStream = driveContents.getOutputStream(); - for (String exportedFilePath : mExportedFiles) { - File exportedFile = new File(exportedFilePath); - FileInputStream fileInputStream = new FileInputStream(exportedFile); - byte[] buffer = new byte[1024]; - int count; - - while ((count = fileInputStream.read(buffer)) >= 0) { - outputStream.write(buffer, 0, count); - } - fileInputStream.close(); - outputStream.flush(); - exportedFile.delete(); - - MetadataChangeSet changeSet = new MetadataChangeSet.Builder() - .setTitle(exportedFile.getName()) - .setMimeType(mExporter.getExportMimeType()) - .build(); - - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); - String folderId = sharedPreferences.getString(mContext.getString(R.string.key_google_drive_app_folder_id), ""); - DriveFolder folder = Drive.DriveApi.getFolder(googleApiClient, DriveId.decodeFromString(folderId)); - // create a file on root folder - folder.createFile(googleApiClient, changeSet, driveContents) - .setResultCallback(fileCallback); - } - } catch (IOException e) { - Crashlytics.logException(e); - Log.e(TAG, e.getMessage()); + DriveApi.DriveContentsResult driveContentsResult = + Drive.DriveApi.newDriveContents(googleApiClient).await(1, TimeUnit.MINUTES); + if (!driveContentsResult.getStatus().isSuccess()) { + throw new Exporter.ExporterException(mExportParams, + "Error while trying to create new file contents"); + } + final DriveContents driveContents = driveContentsResult.getDriveContents(); + DriveFolder.DriveFileResult driveFileResult = null; + try { + // write content to DriveContents + OutputStream outputStream = driveContents.getOutputStream(); + for (String exportedFilePath : mExportedFiles) { + File exportedFile = new File(exportedFilePath); + FileInputStream fileInputStream = new FileInputStream(exportedFile); + byte[] buffer = new byte[1024]; + int count; + + while ((count = fileInputStream.read(buffer)) >= 0) { + outputStream.write(buffer, 0, count); } + fileInputStream.close(); + outputStream.flush(); + exportedFile.delete(); + + MetadataChangeSet changeSet = new MetadataChangeSet.Builder() + .setTitle(exportedFile.getName()) + .setMimeType(mExporter.getExportMimeType()) + .build(); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); + String folderId = sharedPreferences.getString(mContext.getString(R.string.key_google_drive_app_folder_id), ""); + DriveFolder folder = Drive.DriveApi.getFolder(googleApiClient, DriveId.decodeFromString(folderId)); + // create a file on root folder + driveFileResult = folder.createFile(googleApiClient, changeSet, driveContents) + .await(1, TimeUnit.MINUTES); } - }); + } catch (IOException e) { + throw new Exporter.ExporterException(mExportParams, e); + } + + if (driveFileResult == null) + throw new Exporter.ExporterException(mExportParams, "No result received"); + + if (!driveFileResult.getStatus().isSuccess()) + throw new Exporter.ExporterException(mExportParams, "Error creating file in Google Drive"); + + Log.i(TAG, "Created file with id: " + driveFileResult.getDriveFile().getDriveId()); } - private void moveExportToDropbox() { + private void moveExportToDropbox() throws Exporter.ExporterException { Log.i(TAG, "Copying exported file to DropBox"); String dropboxAppKey = mContext.getString(R.string.dropbox_app_key, BackupPreferenceFragment.DROPBOX_APP_KEY); String dropboxAppSecret = mContext.getString(R.string.dropbox_app_secret, BackupPreferenceFragment.DROPBOX_APP_SECRET); @@ -349,13 +314,8 @@ private void moveExportToDropbox() { dbExportFile.writeFromExistingFile(exportedFile, false); exportedFile.delete(); } - } catch (DbxException.Unauthorized unauthorized) { - Crashlytics.logException(unauthorized); - Log.e(TAG, unauthorized.getMessage()); - throw new Exporter.ExporterException(mExportParams); } catch (IOException e) { - Crashlytics.logException(e); - Log.e(TAG, e.getMessage()); + throw new Exporter.ExporterException(mExportParams); } finally { if (dbExportFile != null) { dbExportFile.close(); @@ -363,16 +323,15 @@ private void moveExportToDropbox() { } } - private void moveExportToOwnCloud() { + private void moveExportToOwnCloud() throws Exporter.ExporterException { Log.i(TAG, "Copying exported file to ownCloud"); SharedPreferences mPrefs = mContext.getSharedPreferences(mContext.getString(R.string.owncloud_pref), Context.MODE_PRIVATE); Boolean mOC_sync = mPrefs.getBoolean(mContext.getString(R.string.owncloud_sync), false); - if(!mOC_sync){ - Log.e(TAG, "ownCloud not enabled."); - return; + if (!mOC_sync) { + throw new Exporter.ExporterException(mExportParams, "ownCloud not enabled."); } String mOC_server = mPrefs.getString(mContext.getString(R.string.key_owncloud_server), null); @@ -389,8 +348,10 @@ private void moveExportToOwnCloud() { if (mOC_dir.length() != 0) { RemoteOperationResult dirResult = new CreateRemoteFolderOperation( mOC_dir, true).execute(mClient); - if (!dirResult.isSuccess()) - Log.e(TAG, dirResult.getLogMessage(), dirResult.getException()); + if (!dirResult.isSuccess()) { + Log.w(TAG, "Error creating folder (it may happen if it already exists): " + + dirResult.getLogMessage()); + } } for (String exportedFilePath : mExportedFiles) { String remotePath = mOC_dir + FileUtils.PATH_SEPARATOR + stripPathPart(exportedFilePath); @@ -400,10 +361,9 @@ private void moveExportToOwnCloud() { exportedFilePath, remotePath, mimeType).execute(mClient); if (!result.isSuccess()) - Log.e(TAG, result.getLogMessage(), result.getException()); - else { - new File(exportedFilePath).delete(); - } + throw new Exporter.ExporterException(mExportParams, result.getLogMessage()); + + new File(exportedFilePath).delete(); } } @@ -412,7 +372,7 @@ private void moveExportToOwnCloud() { * external storage, which is accessible to the user. * @return The list of files moved to the SD card. */ - private List moveExportToSDCard() { + private List moveExportToSDCard() throws Exporter.ExporterException { Log.i(TAG, "Moving exported file to external storage"); new File(Exporter.getExportFolderPath(mExporter.mBookUID)); List dstFiles = new ArrayList<>(); @@ -423,8 +383,6 @@ private List moveExportToSDCard() { moveFile(src, dst); dstFiles.add(dst); } catch (IOException e) { - Crashlytics.logException(e); - Log.e(TAG, e.getMessage()); throw new Exporter.ExporterException(mExportParams, e); } } @@ -442,6 +400,7 @@ private String stripPathPart(String fullPathName) { * and deletes all non-template transactions in the database. */ private void backupAndDeleteTransactions(){ + Log.i(TAG, "Backup and deleting transactions after export"); GncXmlExporter.createBackup(); //create backup before deleting everything List openingBalances = new ArrayList<>(); boolean preserveOpeningBalances = GnuCashApplication.shouldSaveOpeningBalances(false); @@ -536,4 +495,48 @@ public void moveFile(String src, String dst) throws IOException { srcFile.delete(); } + private void reportSuccess() { + String targetLocation; + switch (mExportParams.getExportTarget()){ + case SD_CARD: + targetLocation = "SD card"; + break; + case DROPBOX: + targetLocation = "DropBox -> Apps -> GnuCash"; + break; + case GOOGLE_DRIVE: + targetLocation = "Google Drive -> " + mContext.getString(R.string.app_name); + break; + case OWNCLOUD: + targetLocation = mContext.getSharedPreferences( + mContext.getString(R.string.owncloud_pref), + Context.MODE_PRIVATE).getBoolean( + mContext.getString(R.string.owncloud_sync), false) ? + + "ownCloud -> " + + mContext.getSharedPreferences( + mContext.getString(R.string.owncloud_pref), + Context.MODE_PRIVATE).getString( + mContext.getString(R.string.key_owncloud_dir), null) : + "ownCloud sync not enabled"; + break; + default: + targetLocation = mContext.getString(R.string.label_export_target_external_service); + } + Toast.makeText(mContext, + String.format(mContext.getString(R.string.toast_exported_to), targetLocation), + Toast.LENGTH_LONG).show(); + } + + private void refreshViews() { + if (mContext instanceof AccountsActivity){ + AccountsListFragment fragment = + ((AccountsActivity) mContext).getCurrentAccountListFragment(); + if (fragment != null) + fragment.refresh(); + } + if (mContext instanceof TransactionsActivity){ + ((TransactionsActivity) mContext).refresh(); + } + } } diff --git a/app/src/main/java/org/gnucash/android/export/Exporter.java b/app/src/main/java/org/gnucash/android/export/Exporter.java index 88afbabac..c79c9b426 100644 --- a/app/src/main/java/org/gnucash/android/export/Exporter.java +++ b/app/src/main/java/org/gnucash/android/export/Exporter.java @@ -254,7 +254,7 @@ public String getExportMimeType(){ return "text/plain"; } - public static class ExporterException extends RuntimeException{ + public static class ExporterException extends Exception { public ExporterException(ExportParams params){ super("Failed to generate export with parameters: " + params.toString()); diff --git a/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java b/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java index 7b25d0cb4..0e64df8db 100644 --- a/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java +++ b/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java @@ -95,8 +95,6 @@ public List generateExport() throws ExporterException { }, // no recurrence transactions TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_TEMPLATE + " == 0 AND " + - // exclude transactions involving multiple currencies - "trans_extra_info.trans_currency_count = 1 AND " + // in qif, split from the one account entry is not recorded (will be auto balanced) "( " + AccountEntry.TABLE_NAME + "_" + AccountEntry.COLUMN_UID + " != account1." + AccountEntry.COLUMN_UID + " OR " + // or if the transaction has only one split (the whole transaction would be lost if it is not selected) diff --git a/app/src/main/java/org/gnucash/android/model/Recurrence.java b/app/src/main/java/org/gnucash/android/model/Recurrence.java index 48db0af91..830bb0523 100644 --- a/app/src/main/java/org/gnucash/android/model/Recurrence.java +++ b/app/src/main/java/org/gnucash/android/model/Recurrence.java @@ -22,7 +22,6 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.ui.util.RecurrenceParser; -import org.joda.time.DateTime; import org.joda.time.Days; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; @@ -32,8 +31,13 @@ import org.joda.time.Years; import java.sql.Timestamp; +import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; import java.util.Date; +import java.util.List; /** * Model for recurrences in the database @@ -55,9 +59,9 @@ public class Recurrence extends BaseModel { private Timestamp mPeriodEnd; /** - * Describes which day on which to run the recurrence + * Days of week on which to run the recurrence */ - private String mByDay; + private List mByDays = Collections.emptyList(); public Recurrence(@NonNull PeriodType periodType){ setPeriodType(periodType); @@ -131,10 +135,9 @@ public String getRepeatString(){ StringBuilder repeatBuilder = new StringBuilder(mPeriodType.getFrequencyRepeatString()); Context context = GnuCashApplication.getAppContext(); - String dayOfWeek = new SimpleDateFormat("EEEE", GnuCashApplication.getDefaultLocale()) - .format(new Date(mPeriodStart.getTime())); if (mPeriodType == PeriodType.WEEK) { - repeatBuilder.append(" ").append(context.getString(R.string.repeat_on_weekday, dayOfWeek)); + repeatBuilder.append(" "). + append(context.getString(R.string.repeat_on_weekday, getDaysOfWeekString())); } if (mPeriodEnd != null){ @@ -144,7 +147,26 @@ public String getRepeatString(){ return repeatBuilder.toString(); } - /** + /** + * Returns a string with the days of the week set in the recurrence separated by commas. + * @return string with the days of the week set in the recurrence separated by commas. + */ + private @NonNull String getDaysOfWeekString() { + // XXX: mByDays should never be empty with PeriodType.WEEK, but we don't enforce it yet + if (mByDays.isEmpty()) + return ""; + StringBuilder daysOfWeekString = new StringBuilder(); + Calendar calendar = Calendar.getInstance(); + DateFormat dayOfWeekFormatter = + new SimpleDateFormat("EEEE", GnuCashApplication.getDefaultLocale()); + for (int day : mByDays) { + calendar.set(Calendar.DAY_OF_WEEK, day); + daysOfWeekString.append(dayOfWeekFormatter.format(calendar.getTime())).append(", "); + } + return daysOfWeekString.substring(0, daysOfWeekString.length()-2); + } + + /** * Creates an RFC 2445 string which describes this recurring event. *

See http://recurrance.sourceforge.net/

*

The output of this method is not meant for human consumption

@@ -251,19 +273,27 @@ public String getTextOfCurrentPeriod(int periodNum){ } /** - * Sets the string which determines on which day the recurrence will be run - * @param byDay Byday string of recurrence rule (RFC 2445) + * Return the days of week on which to run the recurrence. + * + *

Days are expressed as defined in {@link java.util.Calendar}. + * For example, Calendar.MONDAY

+ * + * @return list of days of week on which to run the recurrence. */ - public void setByDay(String byDay){ - this.mByDay = byDay; + public @NonNull List getByDays(){ + return Collections.unmodifiableList(mByDays); } /** - * Return the byDay string of recurrence rule (RFC 2445) - * @return String with by day specification + * Sets the days on which to run the recurrence. + * + *

Days must be expressed as defined in {@link java.util.Calendar}. + * For example, Calendar.MONDAY

+ * + * @param byDays list of days of week on which to run the recurrence. */ - public String getByDay(){ - return mByDay; + public void setByDays(@NonNull List byDays){ + mByDays = new ArrayList<>(byDays); } /** diff --git a/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java b/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java index 58770873e..e792fc223 100644 --- a/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java +++ b/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java @@ -41,6 +41,7 @@ import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Transaction; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -127,7 +128,7 @@ public static void processScheduledActions(List scheduledAction */ private static void executeScheduledEvent(ScheduledAction scheduledAction, SQLiteDatabase db){ Log.i(LOG_TAG, "Executing scheduled action: " + scheduledAction.toString()); - int executionCount = scheduledAction.getExecutionCount(); + int executionCount = 0; switch (scheduledAction.getActionType()){ case TRANSACTION: @@ -139,20 +140,21 @@ private static void executeScheduledEvent(ScheduledAction scheduledAction, SQLit break; } - //the last run time is computed instead of just using "now" so that if the more than - // one period has been skipped, all intermediate transactions can be created - - scheduledAction.setLastRun(System.currentTimeMillis()); - //set the execution count in the object because it will be checked for the next iteration in the calling loop - scheduledAction.setExecutionCount(executionCount); //this call is important, do not remove!! - //update the last run time and execution count - ContentValues contentValues = new ContentValues(); - contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_LAST_RUN, - scheduledAction.getLastRunTime()); - contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_EXECUTION_COUNT, - scheduledAction.getExecutionCount()); - db.update(DatabaseSchema.ScheduledActionEntry.TABLE_NAME, contentValues, - DatabaseSchema.ScheduledActionEntry.COLUMN_UID + "=?", new String[]{scheduledAction.getUID()}); + if (executionCount > 0) { + scheduledAction.setLastRun(System.currentTimeMillis()); + // Set the execution count in the object because it will be checked + // for the next iteration in the calling loop. + // This call is important, do not remove!! + scheduledAction.setExecutionCount(scheduledAction.getExecutionCount() + executionCount); + // Update the last run time and execution count + ContentValues contentValues = new ContentValues(); + contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_LAST_RUN, + scheduledAction.getLastRunTime()); + contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_EXECUTION_COUNT, + scheduledAction.getExecutionCount()); + db.update(DatabaseSchema.ScheduledActionEntry.TABLE_NAME, contentValues, + DatabaseSchema.ScheduledActionEntry.COLUMN_UID + "=?", new String[]{scheduledAction.getUID()}); + } } /** @@ -167,6 +169,8 @@ private static int executeBackup(ScheduledAction scheduledAction, SQLiteDatabase return 0; ExportParams params = ExportParams.parseCsv(scheduledAction.getTag()); + // HACK: the tag isn't updated with the new date, so set the correct by hand + params.setExportStartTime(new Timestamp(scheduledAction.getLastRunTime())); try { //wait for async task to finish before we proceed (we are holding a wake lock) new ExportAsyncTask(GnuCashApplication.getAppContext(), db).execute(params).get(); @@ -219,6 +223,7 @@ private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDa int totalPlannedExecutions = scheduledAction.getTotalPlannedExecutionCount(); List transactions = new ArrayList<>(); + int previousExecutionCount = scheduledAction.getExecutionCount(); // We'll modify it //we may be executing scheduled action significantly after scheduled time (depending on when Android fires the alarm) //so compute the actual transaction time from pre-known values long transactionTime = scheduledAction.computeNextCountBasedScheduledExecutionTime(); @@ -235,6 +240,8 @@ private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDa } transactionsDbAdapter.bulkAddRecords(transactions, DatabaseAdapter.UpdateMethod.insert); + // Be nice and restore the parameter's original state to avoid confusing the callers + scheduledAction.setExecutionCount(previousExecutionCount); return executionCount; } } diff --git a/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java b/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java index 066e68d10..44c7746e6 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java @@ -87,7 +87,7 @@ public class AccountFormFragment extends Fragment { /** * Tag for the color picker dialog fragment */ - public static final String COLOR_PICKER_DIALOG_TAG = "color_picker_dialog"; + private static final String COLOR_PICKER_DIALOG_TAG = "color_picker_dialog"; /** * EditText for the name of the account to be created/edited @@ -176,7 +176,7 @@ public class AccountFormFragment extends Fragment { /** * Spinner for selecting the default transfer account */ - @Bind(R.id.input_default_transfer_account) Spinner mDefaulTransferAccountSpinner; + @Bind(R.id.input_default_transfer_account) Spinner mDefaultTransferAccountSpinner; /** * Account description input text view @@ -205,13 +205,14 @@ public class AccountFormFragment extends Fragment { */ @Bind(R.id.input_color_picker) ColorSquare mColorSquare; - private ColorPickerSwatch.OnColorSelectedListener mColorSelectedListener = new ColorPickerSwatch.OnColorSelectedListener() { - @Override - public void onColorSelected(int color) { - mColorSquare.setBackgroundColor(color); - mSelectedColor = color; - } - }; + private final ColorPickerSwatch.OnColorSelectedListener mColorSelectedListener = + new ColorPickerSwatch.OnColorSelectedListener() { + @Override + public void onColorSelected(int color) { + mColorSquare.setBackgroundColor(color); + mSelectedColor = color; + } + }; /** @@ -294,11 +295,11 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { } }); - mDefaulTransferAccountSpinner.setEnabled(false); + mDefaultTransferAccountSpinner.setEnabled(false); mDefaultTransferAccountCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { - mDefaulTransferAccountSpinner.setEnabled(isChecked); + mDefaultTransferAccountSpinner.setEnabled(isChecked); } }); @@ -408,6 +409,7 @@ private void initializeViewsWithAccount(Account account){ } mPlaceholderCheckBox.setChecked(account.isPlaceholderAccount()); + mSelectedColor = account.getColor(); mColorSquare.setBackgroundColor(account.getColor()); setAccountTypeSelection(account.getAccountType()); @@ -495,13 +497,13 @@ private void setParentAccountSelection(long parentAccountId){ private void setDefaultTransferAccountSelection(long defaultTransferAccountId, boolean enableTransferAccount) { if (defaultTransferAccountId > 0) { mDefaultTransferAccountCheckBox.setChecked(enableTransferAccount); - mDefaulTransferAccountSpinner.setEnabled(enableTransferAccount); + mDefaultTransferAccountSpinner.setEnabled(enableTransferAccount); } else return; for (int pos = 0; pos < mDefaultTransferAccountCursorAdapter.getCount(); pos++) { if (mDefaultTransferAccountCursorAdapter.getItemId(pos) == defaultTransferAccountId) { - mDefaulTransferAccountSpinner.setSelection(pos); + mDefaultTransferAccountSpinner.setSelection(pos); break; } } @@ -520,6 +522,7 @@ private int[] getAccountColorOptions(){ int color = colorTypedArray.getColor(i, getResources().getColor(R.color.title_green)); colorOptions[i] = color; } + colorTypedArray.recycle(); return colorOptions; } /** @@ -573,13 +576,13 @@ private void loadDefaultTransferAccountList(){ Cursor defaultTransferAccountCursor = mAccountsDbAdapter.fetchAccountsOrderedByFullName(condition, new String[]{AccountType.ROOT.name()}); - if (mDefaulTransferAccountSpinner.getCount() <= 0) { + if (mDefaultTransferAccountSpinner.getCount() <= 0) { setDefaultTransferAccountInputsVisible(false); } mDefaultTransferAccountCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), defaultTransferAccountCursor); - mDefaulTransferAccountSpinner.setAdapter(mDefaultTransferAccountCursorAdapter); + mDefaultTransferAccountSpinner.setAdapter(mDefaultTransferAccountCursorAdapter); } /** @@ -732,7 +735,7 @@ private void saveAccount() { boolean nameChanged = false; if (mAccount == null){ String name = getEnteredName(); - if (name == null || name.length() == 0){ + if (name.length() == 0){ mTextInputLayout.setErrorEnabled(true); mTextInputLayout.setError(getString(R.string.toast_no_account_name_entered)); return; @@ -770,8 +773,8 @@ private void saveAccount() { mAccount.setParentUID(newParentAccountUID); if (mDefaultTransferAccountCheckBox.isChecked() - && mDefaulTransferAccountSpinner.getSelectedItemId() != Spinner.INVALID_ROW_ID){ - long id = mDefaulTransferAccountSpinner.getSelectedItemId(); + && mDefaultTransferAccountSpinner.getSelectedItemId() != Spinner.INVALID_ROW_ID){ + long id = mDefaultTransferAccountSpinner.getSelectedItemId(); mAccount.setDefaultTransferAccountUID(mAccountsDbAdapter.getUID(id)); } else { //explicitly set in case of removal of default account @@ -841,7 +844,7 @@ private AccountType getSelectedAccountType() { * Retrieves the name of the account which has been entered in the EditText * @return Name of the account which has been entered in the EditText */ - public String getEnteredName(){ + private String getEnteredName(){ return mNameEditText.getText().toString().trim(); } diff --git a/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java b/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java index 1b72ed8f3..5f8b47e9b 100644 --- a/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java @@ -74,6 +74,7 @@ public class ReportsActivity extends BaseDrawerActivity implements AdapterView.O Color.parseColor("#ba037c"), Color.parseColor("#708809"), Color.parseColor("#32072c"), Color.parseColor("#fddef8"), Color.parseColor("#fa0e6e"), Color.parseColor("#d9e7b5") }; + private static final String STATE_REPORT_TYPE = "STATE_REPORT_TYPE"; @Bind(R.id.time_range_spinner) Spinner mTimeRangeSpinner; @Bind(R.id.report_account_type_spinner) Spinner mAccountTypeSpinner; @@ -123,8 +124,11 @@ public int getTitleRes() { @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + mReportType = (ReportType) savedInstanceState.getSerializable(STATE_REPORT_TYPE); + } + super.onCreate(savedInstanceState); mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); ArrayAdapter adapter = ArrayAdapter.createFromResource(this, R.array.report_time_range, @@ -414,4 +418,11 @@ public void refresh() { public void refresh(String uid) { refresh(); } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putSerializable(STATE_REPORT_TYPE, mReportType); + } } diff --git a/app/src/main/java/org/gnucash/android/ui/settings/BookManagerFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/BookManagerFragment.java index ff7fa0e67..718b8602c 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/BookManagerFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/BookManagerFragment.java @@ -54,6 +54,8 @@ import org.gnucash.android.ui.common.Refreshable; import org.gnucash.android.util.PreferencesHelper; +import java.sql.Timestamp; + /** * Fragment for managing the books in the database */ @@ -159,25 +161,8 @@ public void bindView(View view, final Context context, Cursor cursor) { final String bookUID = cursor.getString(cursor.getColumnIndexOrThrow(BookEntry.COLUMN_UID)); - TextView lastSyncText = (TextView) view.findViewById(R.id.last_sync_time); - lastSyncText.setText(PreferencesHelper.getLastExportTime(bookUID).toString()); - - TextView labelLastSync = (TextView) view.findViewById(R.id.label_last_sync); - labelLastSync.setText(R.string.label_last_export_time); - - //retrieve some book statistics - DatabaseHelper dbHelper = new DatabaseHelper(GnuCashApplication.getAppContext(), bookUID); - SQLiteDatabase db = dbHelper.getReadableDatabase(); - TransactionsDbAdapter trnAdapter = new TransactionsDbAdapter(db, new SplitsDbAdapter(db)); - int transactionCount = (int) trnAdapter.getRecordsCount(); - String transactionStats = getResources().getQuantityString(R.plurals.book_transaction_stats, transactionCount, transactionCount); - - AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(db, trnAdapter); - int accountsCount = (int) accountsDbAdapter.getRecordsCount(); - String accountStats = getResources().getQuantityString(R.plurals.book_account_stats, accountsCount, accountsCount); - String stats = accountStats + ", " + transactionStats; - TextView statsText = (TextView) view.findViewById(R.id.secondary_text); - statsText.setText(stats); + setLastExportedText(view, bookUID); + setStatisticsText(view, bookUID); ImageView optionsMenu = (ImageView) view.findViewById(R.id.options_menu); optionsMenu.setOnClickListener(new View.OnClickListener() { @@ -248,6 +233,33 @@ public void onClick(View v) { } }); } + + private void setLastExportedText(View view, String bookUID) { + TextView labelLastSync = (TextView) view.findViewById(R.id.label_last_sync); + labelLastSync.setText(R.string.label_last_export_time); + + Timestamp lastSyncTime = PreferencesHelper.getLastExportTime(bookUID); + TextView lastSyncText = (TextView) view.findViewById(R.id.last_sync_time); + if (lastSyncTime.equals(new Timestamp(0))) + lastSyncText.setText(R.string.last_export_time_never); + else + lastSyncText.setText(lastSyncTime.toString()); + } + + private void setStatisticsText(View view, String bookUID) { + DatabaseHelper dbHelper = new DatabaseHelper(GnuCashApplication.getAppContext(), bookUID); + SQLiteDatabase db = dbHelper.getReadableDatabase(); + TransactionsDbAdapter trnAdapter = new TransactionsDbAdapter(db, new SplitsDbAdapter(db)); + int transactionCount = (int) trnAdapter.getRecordsCount(); + String transactionStats = getResources().getQuantityString(R.plurals.book_transaction_stats, transactionCount, transactionCount); + + AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(db, trnAdapter); + int accountsCount = (int) accountsDbAdapter.getRecordsCount(); + String accountStats = getResources().getQuantityString(R.plurals.book_account_stats, accountsCount, accountsCount); + String stats = accountStats + ", " + transactionStats; + TextView statsText = (TextView) view.findViewById(R.id.secondary_text); + statsText.setText(stats); + } } /** diff --git a/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java b/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java index 6309db40d..379dd60f7 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java +++ b/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java @@ -16,6 +16,8 @@ package org.gnucash.android.ui.util; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.format.Time; import com.codetroopers.betterpickers.recurrencepicker.EventRecurrence; @@ -24,7 +26,10 @@ import org.gnucash.android.model.Recurrence; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; +import java.util.List; /** * Parses {@link EventRecurrence}s to generate @@ -76,7 +81,7 @@ public static Recurrence parse(EventRecurrence eventRecurrence){ periodType.setMultiplier(interval); Recurrence recurrence = new Recurrence(periodType); parseEndTime(eventRecurrence, recurrence); - recurrence.setByDay(parseByDay(eventRecurrence.byday)); + recurrence.setByDays(parseByDay(eventRecurrence.byday)); if (eventRecurrence.startDate != null) recurrence.setPeriodStart(new Timestamp(eventRecurrence.startDate.toMillis(false))); @@ -85,7 +90,7 @@ public static Recurrence parse(EventRecurrence eventRecurrence){ /** * Parses the end time from an EventRecurrence object and sets it to the scheduledEvent. - * The end time is specified in the dialog either by number of occurences or a date. + * The end time is specified in the dialog either by number of occurrences or a date. * @param eventRecurrence Event recurrence pattern obtained from dialog * @param recurrence Recurrence event to set the end period to */ @@ -100,90 +105,24 @@ private static void parseEndTime(EventRecurrence eventRecurrence, Recurrence rec } /** - * Returns the date for the next day of the week - * @param dow Day of the week (Calendar constants) - * @return Calendar instance with the next day of the week + * Parses an array of byDay values to return a list of days of week + * constants from {@link Calendar}. + * + *

Currently only supports byDay values for weeks.

+ * + * @param byDay Array of byDay values + * @return list of days of week constants from Calendar. */ - private static Calendar nextDayOfWeek(int dow) { - Calendar date = Calendar.getInstance(); - int diff = dow - date.get(Calendar.DAY_OF_WEEK); - if (!(diff > 0)) { - diff += 7; + private static @NonNull List parseByDay(@Nullable int[] byDay) { + if (byDay == null) { + return Collections.emptyList(); } - date.add(Calendar.DAY_OF_MONTH, diff); - return date; - } - /** - * Parses an array of byday values to return the string concatenation of days of the week. - *

Currently only supports byDay values for weeks

- * @param byday Array of byday values - * @return String concat of days of the week or null if {@code byday} was empty - */ - private static String parseByDay(int[] byday){ - if (byday == null || byday.length == 0){ - return null; - } - //todo: parse for month and year as well, when our dialog supports those - StringBuilder builder = new StringBuilder(); - for (int day : byday) { - switch (day) - { - case EventRecurrence.SU: - builder.append("SU"); - break; - case EventRecurrence.MO: - builder.append("MO"); - break; - case EventRecurrence.TU: - builder.append("TU"); - break; - case EventRecurrence.WE: - builder.append("WE"); - break; - case EventRecurrence.TH: - builder.append("TH"); - break; - case EventRecurrence.FR: - builder.append("FR"); - break; - case EventRecurrence.SA: - builder.append("SA"); - break; - default: - throw new RuntimeException("bad day of week: " + day); - } - builder.append(","); + List byDaysList = new ArrayList<>(byDay.length); + for (int day : byDay) { + byDaysList.add(EventRecurrence.day2CalendarDay(day)); } - builder.deleteCharAt(builder.length()-1); - return builder.toString(); - } - /** - * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY - * constants. btw, I think we should switch to those here too, to - * get rid of this function, if possible. - */ - public static int day2CalendarDay(int day) - { - switch (day) - { - case EventRecurrence.SU: - return Calendar.SUNDAY; - case EventRecurrence.MO: - return Calendar.MONDAY; - case EventRecurrence.TU: - return Calendar.TUESDAY; - case EventRecurrence.WE: - return Calendar.WEDNESDAY; - case EventRecurrence.TH: - return Calendar.THURSDAY; - case EventRecurrence.FR: - return Calendar.FRIDAY; - case EventRecurrence.SA: - return Calendar.SATURDAY; - default: - throw new RuntimeException("bad day of week: " + day); - } + return byDaysList; } } diff --git a/app/src/main/java/org/gnucash/android/ui/util/dialog/DateRangePickerDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/util/dialog/DateRangePickerDialogFragment.java index 954c10f2e..1447119d1 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/dialog/DateRangePickerDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/util/dialog/DateRangePickerDialogFragment.java @@ -52,6 +52,7 @@ public class DateRangePickerDialogFragment extends DialogFragment{ private Date mStartRange = LocalDate.now().minusMonths(1).toDate(); private Date mEndRange = LocalDate.now().toDate(); private OnDateRangeSetListener mDateRangeSetListener; + private static final long ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000; public static DateRangePickerDialogFragment newInstance(OnDateRangeSetListener dateRangeSetListener){ DateRangePickerDialogFragment fragment = new DateRangePickerDialogFragment(); @@ -89,7 +90,11 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa public void onClick(View v) { List selectedDates = mCalendarPickerView.getSelectedDates(); Date startDate = selectedDates.get(0); - Date endDate = selectedDates.size() == 2 ? selectedDates.get(1) : new Date(); + // If only one day is selected (no interval) start and end should be the same (the selected one) + Date endDate = selectedDates.size() > 1 ? selectedDates.get(selectedDates.size() - 1) : new Date(startDate.getTime()); + // CaledarPicker returns the start of the selected day but we want all transactions of that day to be included. + // Therefore we have to add 24 hours to the endDate. + endDate.setTime(endDate.getTime() + ONE_DAY_IN_MILLIS); mDateRangeSetListener.onDateRangeSet(startDate, endDate); dismiss(); } diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 2603b4b90..a21786e46 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -18,7 +18,7 @@ Create Account Edit Account - Info + Čestina Export… Add a new transaction to an account No accounts to display diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index 46d68e911..68f76a3bb 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -19,13 +19,13 @@ Crear Cuenta Editar Cuenta Detalles - Exportar OFX + Exportar… Añadir una nueva transacción a una cuenta No hay cuentas que mostrar Nombre de la cuenta Cancelar Guardar - Test + Prueba Introducir contraseña Contraseña incorrecta, intentar otra vez Contraseña establecida @@ -52,13 +52,13 @@ MOVER %1$d seleccionado Saldo: - Export To: - Export Transactions + Exportar A: + Exportar Transacciones Exportar todas las transacciones Por defecto solo las nuevas transacciones desde la última exportación serán exportadas. Seleccione esta opción para exportar todas las transacciones Error exportando datos %1$s Exportar - Delete transactions after export + Borrar transacciones despues de exportar Todas las transacciones exportadas serán borradas cuando la exportación haya terminado Ajustes @@ -66,7 +66,7 @@ DropBox Google Drive ownCloud - Send to… + Enviar a… Mover Mover %1$d transacción(es) @@ -82,8 +82,8 @@ Permite crear cuentas en Gnucash para Android Sus datos GnuCash Leer y modificar datos Gnucash - Record transactions in GnuCash - Create accounts in GnuCash + Registrar transacciones en GnuCash + Crear cuentas en GnuCash Mostrar cuentas Crear Cuentas Seleccionar cuentas a crear @@ -132,13 +132,12 @@ Active esta opción para exportar a otras aplicaciones distintas a GnuCash para escritorio Novedades - - Support for multiple different books \n - - Adds ownCloud as destination for exports\n - - Compact view for transactions list\n - - Re-design of passcode lock screen\n - - Improved handling of scheduled transactions\n - - Multiple bug fixes and improvements\n - + - Soporte de múltiples libros\n +- Añade ownCloud como destino para exportaciones\n +- Vista compacta para la lista de transacciones\n +- Rediseño de la pantalla de bloqueo con contraseña\n +- Mejora del manejo de transacciones programadas\n +- Múltiples fallos solucionados y otras mejoras\n
Cerrar Introduzca un importe para guardar la transacción Las transacciones multi-divisa so se pueden modificar @@ -211,13 +210,13 @@ Todas Crea una estructura por defecto de cuentas GnuCash comúnmente usadas Crear cuentas por defecto - A new book will be opened with the default accounts\n\nYour current accounts and transactions will not be modified! + Se abrirá un nuevo libro con las cuentas por defecto\n\nSus cuentas y transacciones actuales no se modificarán! Transacciones Programadas ¡Bienvenido a GnuCash Android! \nPuede crear una jerarquía de cuentas comúnmente usadas o importar su propia estructura de cuentas GnuCash. \n\nAmbas opciones están disponibles en las opciones de la aplicació por si quiere decidirlo más tarde. Transacciones Programadas Seleccionar destino para exportar - Memo + Nota Gastar Recibir Sacar @@ -252,7 +251,7 @@ Gráfico de tarta Gráfico de línea Gráfico de barras - Report Preferences + Preferencias de informe Seleccionar divisa Color de la cuenta en los informes Usar el color de la cuenta en las gráficas de barra y tarta @@ -277,11 +276,11 @@ Toque para crear la programación Restaurar copia de seguridad… Copia de seguridad y exportación - Enable DropBox - Enable ownCloud + Habilitar DropBox + Habilitar ownCloud Copia de seguridad - Enable exporting to DropBox - Enable exporting to ownCloud + Habilitar exportar a DropBox + Habilitar exportar a ownCloud Seleccionar archivo XML GnuCash Preferencias de copia de seguridad Crear copia de seguridad @@ -290,8 +289,8 @@ Copia de seguridad exitosa Copia de seguridad fallida Exportar todas las cuentas y transacciones - Enable Google Drive - Enable exporting to Google Drive + Habilitar Google Drive + Habilitar exportar a Google Drive Instale un gestor de archivos para seleccionar archivos Seleccione la copia de seguridad a restaurar Favoritos @@ -300,16 +299,16 @@ Transacciones Programadas Exportar… Ajustes - User Name - Password - owncloud + Nombre de usuario + Contraseña + ownCloud https:// - OC server not found - OC username/password invalid - Invalid chars: \\ < > : \" | * ? - OC server OK - OC username/password OK - Dir name OK + Servidor OC no encontrado + Usuario/contraseña OC no válidos + Caracteres inválidos: \\ < > : \" | * ? + Servidor OC correcto + Usuario/contraseña OC correctos + Nombre del directorio correcto Diario Cada %d días @@ -393,21 +392,21 @@ Este proceso solo recoge información que no permite identificar al usuarioAño
Hoja de Balance Total: - Google+ Community - Translate GnuCash - Share ideas, discuss changes or report problems - Translate or proof-read on CrowdIn - No compatible apps to receive the exported transactions! - Move… - Duplicate - Budgets - Cash Flow - Budgets - Enable compact view - Enable to always use compact view for transactions list - Invalid exchange rate - e.g. 1 %1$s = x.xx %2$s - Invalid amount + Comunidad Google+ + Traducir GnuCash + Compartir ideas, discutir cambios o reportar problemas + Traducir o revisar en CrowdIn + ¡No hay aplicaciones compatibles para recibir las transacciones exportadas! + Mover… + Duplicar + Presupuestos + Flujo de efectivo + Presupuestos + Activar la vista compacta + Activar para usar siempre la vista compacta en la lista de transacciones + Tipo de cambio no válido + ej. 1 %1$s = x.xx %2$s + Cantidad inválida Mes actual Últimos 3 meses @@ -474,5 +473,5 @@ Este proceso solo recoge información que no permite identificar al usuarioon %1$s
for %1$s times Compact View - Book %1$d + Libro %1$d diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 0c1a4909f..07e296b45 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -16,45 +16,45 @@ limitations under the License. --> - Create Account - Edit Account - Info - Export… - Add a new transaction to an account - No accounts to display - Account name - Cancel - Save - Test - Enter Passcode - Wrong passcode, please try again - Passcode set - Please confirm your passcode - Invalid passcode confirmation. Please try again - Description - Amount - New transaction - No transactions to display + Luo tili + Muokkaa tiliä + Tiedot + Vie… + Lisää uusi tapahtuma tilille + Ei näytettävissä olevia tilejä + Tilin nimi + Peruuta + Tallenna + Testaa + Anna tunnuskoodi + Virheellinen tunnuskoodi, yritä uudelleen + Tunnuskoodi asetettu + Vahvista tunnuskoodi + Virheellinen tunnuskoodin vahvistus. Yritä uudelleen + Kuvaus + Summa + Uusi tapahtuma + Ei näytettäviä tapahtumia DATE & TIME - Account - DEBIT - CREDIT - Accounts - Transactions - Delete - Delete - Cancel - Account deleted - Confirm delete - All transactions in this account will also be deleted - Edit Transaction - Add note - MOVE - %1$d selected - Balance: + Tili + VELOITUS + HYVITYS + Tilit + Tapahtumat + Poista + Poista + Peruuta + Tili poistettu + Vahvista poistaminen + Kaikki tämän tilin tapahtumat tullaan myös poistamaan + Muokkaa tapahtumaa + Lisää huomautus + SIIRRÄ + %1$d valittuna + Saldo: Export To: - Export Transactions - Export all transactions + Vie tapahtumat + Vie kaikki tapahtumat By default, only new transactions since last export will be exported. Check this option to export all transactions Error exporting %1$s file Export diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c7c682862..a264ad7a1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -52,13 +52,13 @@ DÉPLACER %1$d sélectionné(s) Solde: - Export To: + Exporter vers: Exporter les transactions Exporter toutes les transactions Par défaut, seul les nouvelles transactions depuis le dernier export seront exportées. Cochez cette option pour exporter toutes les transactions Erreur lors de l\'export des données en %1$s Exporter - Delete transactions after export + Effacer les transactions apres export Toutes les transactions exportées seront supprimées aprés l\'export Paramètres @@ -66,7 +66,7 @@ DropBox Google Drive ownCloud - Send to… + Envoyer vers… Déplacer Déplacer %1$d transaction(s) @@ -82,8 +82,8 @@ Permettre la création de comptes dans GnuCash pour Android Vos données GnuCash Lire et modifier les données GnuCash - Record transactions in GnuCash - Create accounts in GnuCash + Enregistrer les transactions dans GnuCash + Créer comptes dans GnuCash Afficher le compte Créer les comptes Choisisez les comptes à créés @@ -116,7 +116,7 @@ Êtes vous sûre de vouloir supprimer TOUTES les transactions ? Êtes vous sûre de vouloir supprimer cette transaction ? - Export + Exporter Exporter toutes les transactions Supprimer les transactions exportées Email d\'export par défaut @@ -124,7 +124,7 @@ Transfert entre comptes Toutes les transactions seront transférées d\'un compte à l\'autre Activer les doubles entrée - Balance + Solde Entrer un nom de compte pour créer un compte Monnaie Compte parent @@ -259,7 +259,7 @@ Tri par taille Show legend Show labels - Show percentage + Montrer pourcentage Show average lines Groupe par petites tranches Aucune données de graphe disponible @@ -276,11 +276,11 @@ Appuyez sur pour créer le calendrier Restaurer Sauvegarde… Sauvegarde & export - Enable DropBox - Enable ownCloud + Activer DropBox + Activer ownCloud Sauvegarde - Enable exporting to DropBox - Enable exporting to ownCloud + Activer export vers DropBox + Activer export vers owncloud Selectionner un fichier GnuCash XML Sauvergarde Préférences Créer Sauvegarde @@ -289,8 +289,8 @@ Sauvegarde réussie Échec de la sauvegarde Exporte tous les comptes et les transactions - Enable Google Drive - Enable exporting to Google Drive + Activer Google Drive + Activer export vers Google Drive Installez un gestionnaire de fichiers pour sélectionner les fichiers Sélectionnez une sauvegarde à restaurer Favoris @@ -299,11 +299,11 @@ Transactions planifiées Export… Paramètres - User Name - Password - owncloud + Nom d\'utilisateur + Mot de passe + ownCloud https:// - OC server not found + Serveur OC inaccessible OC username/password invalid Invalid chars: \\ < > : \" | * ? OC server OK diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml new file mode 100644 index 000000000..1df061e4a --- /dev/null +++ b/app/src/main/res/values-in-rID/strings.xml @@ -0,0 +1,474 @@ + + + + + Create Account + Edit Account + Info + Export… + Add a new transaction to an account + No accounts to display + Account name + Cancel + Save + Test + Enter Passcode + Wrong passcode, please try again + Passcode set + Please confirm your passcode + Invalid passcode confirmation. Please try again + Description + Amount + New transaction + No transactions to display + DATE & TIME + Account + DEBIT + CREDIT + Accounts + Transactions + Delete + Delete + Cancel + Account deleted + Confirm delete + All transactions in this account will also be deleted + Edit Transaction + Add note + MOVE + %1$d selected + Balance: + Export To: + Export Transactions + Export all transactions + By default, only new transactions since last export will be exported. Check this option to export all transactions + Error exporting %1$s file + Export + Delete transactions after export + All exported transactions will be deleted when exporting is completed + Settings + + SD Card + DropBox + Google Drive + ownCloud + Send to… + + Move + Move %1$d transaction(s) + Destination Account + Access SD Card + Cannot move transactions.\nThe destination account uses a different currency from origin account + General + About + Choose default currency + Default currency + Default currency to assign to new accounts + Enables recording transactions in GnuCash for Android + Enables creation of accounts in GnuCash for Android + Your GnuCash data + Read and modify GnuCash data + Record transactions in GnuCash + Create accounts in GnuCash + Display account + Create Accounts + Select accounts to create + No accounts exist in GnuCash.\nCreate an account before adding a widget + Build version + License + Apache License v2.0. Click for details + General Preferences + Select Account + There are no transactions available to export + Passcode + Passcode Preferences + Passcode Turned On + Passcode Turned Off + Change Passcode + About GnuCash + A mobile finance management and expense-tracker designed for Android + About + %1$s file exported to:\n + GnuCash Android %1$s export + GnuCash Android Export from + Transactions + Transaction Preferences + Account Preferences + Default Transaction Type + The type of transaction to use by default, CREDIT or DEBIT + + CREDIT + DEBIT + + Are you sure you want to delete ALL transactions? + Are you sure you want to delete this transaction? + Export + Export all transactions + Delete exported transactions + Default export email + The default email address to send exports to. You can still change this when you export. + Transfer Account + All transactions will be a transfer from one account to another + Activate Double Entry + Balance + Enter an account name to create an account + Currency + Parent account + Use XML OFX header + Enable this option when exporting to third-party application other than GnuCash for desktop + What\'s New + + - Support for multiple different books \n + - Adds ownCloud as destination for exports\n + - Compact view for transactions list\n + - Re-design of passcode lock screen\n + - Improved handling of scheduled transactions\n + - Multiple bug fixes and improvements\n + + Dismiss + Enter an amount to save the transaction + Multi-currency transactions cannot be modified + Import GnuCash Accounts + Import Accounts + An error occurred while importing the GnuCash accounts + GnuCash Accounts successfully imported + Import account structure exported from GnuCash desktop + Import GnuCash XML + Delete all accounts in the database. All transactions will be deleted as + well. + + Delete all accounts + Accounts + All accounts have been successfully deleted + Are you sure you want to delete all accounts and transactions?\n\nThis + operation cannot be undone! + + Account Type + All transactions in all accounts will be deleted! + Delete all transactions + All transactions successfully deleted! + Importing accounts + Tap again to confirm. ALL entries will be deleted!! + Transactions + Sub-Accounts + Search + Default Export Format + File format to use by default when exporting transactions + Export transactions… + Recurrence + + Imbalance + Exporting transactions + No recurring transactions to display. + Successfully deleted recurring transaction + Placeholder account + Default Transfer Account + + %d sub-accounts + + + CASH + BANK + CREDIT CARD + ASSET + LIABILITY + INCOME + EXPENSE + PAYABLE + RECEIVABLE + EQUITY + CURRENCY + STOCK + MUTUAL FUND + TRADING + + + QIF + OFX + XML + + + Select a Color + + + Account Color & Type + Delete sub-accounts + Recent + Favorites + All + Creates default GnuCash commonly-used account structure + Create default accounts + A new book will be opened with the default accounts\n\nYour current accounts and transactions will not be modified! + Scheduled Transactions + Welcome to GnuCash Android! \nYou can either create + a hierarchy of commonly-used accounts, or import your own GnuCash account structure. \n\nBoth options are also + available in app Settings so you can decide later. + + Transactions + Select destination for export + Memo + Spend + Receive + Withdrawal + Deposit + Payment + Charge + Decrease + Increase + Income + Rebate + Expense + Bill + Invoice + Buy + Sell + Repeats + No recent backup found + Opening Balances + Equity + Enable to save the current account balance (before deleting transactions) as new opening balance after deleting transactions + + Save account opening balances + OFX does not support double-entry transactions + Generates separate QIF files per currency + Transaction splits + Imbalance: + Add split + Favorite + Navigation drawer opened + Navigation drawer closed + Reports + Pie Chart + Line Chart + Bar Chart + Report Preferences + Select currency + Account color in reports + Use account color in the bar/pie chart + Reports + Order by size + Show legend + Show labels + Show percentage + Show average lines + Group Smaller Slices + No chart data available + Overall + Total + Other + The percentage of selected value calculated from the total amount + The percentage of selected value calculated from the current stacked bar amount + Save as template + This account contains transactions. \nWhat would you like to do with these transactions + This account contains sub-accounts. \nWhat would you like to do with these sub-accounts + Delete transactions + Create and specify a transfer account OR disable double-entry in settings to save the transaction + Tap to create schedule + Restore Backup… + Backup & export + Enable DropBox + Enable ownCloud + Backup + Enable exporting to DropBox + Enable exporting to ownCloud + Select GnuCash XML file + Backup Preferences + Create Backup + By default backups are saved to the SDCARD + Select a specific backup to restore + Backup successful + Backup failed + Exports all accounts and transactions + Enable Google Drive + Enable exporting to Google Drive + Install a file manager to select files + Select backup to restore + Favorites + Open… + Reports + Scheduled Transactions + Export… + Settings + User Name + Password + owncloud + https:// + OC server not found + OC username/password invalid + Invalid chars: \\ < > : \" | * ? + OC server OK + OC username/password OK + Dir name OK + + Every %d days + + + Every %d weeks + + + Every %d months + + + Every %d years + + Enable Crash Logging + Automatically send information about app malfunction to the developers. + Format + Backup folder cannot be found. Make sure the SD Card is mounted! + Enter your old passcode + Enter your new passcode + Scheduled Exports + Exports + No scheduled exports to display + Create export schedule + Exported to: %1$s + The legend is too long + Account description + No recent accounts + No favorite accounts + Scheduled Actions + "Ended, last executed on %1$s" + Select a bar to view details + Next + Done + Default Currency + Account Setup + Select Currency + Feedback Options + Create default accounts + Import my accounts + Let me handle it + Other… + Automatically send crash reports + Disable crash reports + Back + Setup GnuCash + Welcome to GnuCash + Before you dive in, \nlet\'s setup a few things first\n\nTo continue, press Next + Split Editor + Check that all splits have valid amounts before saving! + Invalid expression! + Scheduled recurring transaction + An exchange rate is required + The converted amount is required + Transfer Funds + + Select a slice to see details + Period: + From: + To: + Provide either the converted amount or exchange rate in order to transfer funds + Exchange rate + Fetch quote + Converted Amount + Sheet + Expenses for last 3 months + Total Assets + Total Liabilities + Net Worth + Assets + Liabilities + Equity + + Move to: + Group By + Month + Quarter + Year + Balance Sheet + Total: + Google+ Community + Translate GnuCash + Share ideas, discuss changes or report problems + Translate or proof-read on CrowdIn + No compatible apps to receive the exported transactions! + Move… + Duplicate + Budgets + Cash Flow + Budgets + Enable compact view + Enable to always use compact view for transactions list + Invalid exchange rate + e.g. 1 %1$s = x.xx %2$s + Invalid amount + + Current month + Last 3 months + Last 6 months + Last 12 months + All time + Custom range… + + + 1 + + + 2 + ABC + 3 + DEF + 4 + GHI + 5 + JKL + 6 + MNO + 7 + PQRS + 8 + TUV + 9 + WXYZ + 0 + + + Manage Books + Manage Books… + Select any part of the chart to view details + Confirm delete Book + All accounts and transactions in this book will be deleted! + Delete Book + Last Exported: + Enable Sync + New Book + The selected transaction has no splits and cannot be opened + %1$d splits + in %1$s + + %d accounts + + + %d transactions + + + EXPENSE + INCOME + + Connected to Google Drive + Unable to connect to Google Drive + Please enter an amount to split + external service + Updated transaction recurring schedule + Since + All time + Recommend in Play Store + until %1$s + on %1$s + for %1$s times + Compact View + Book %1$d + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 404cbe75f..f3b7b3965 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -449,28 +449,28 @@ Neste processo não serão recolhidas informações do utilizador!
%1$d divisões em %1$s - %d account - %d accounts + %d conta + %d contas - %d transaction - %d transactions + %d transação + %d transações - EXPENSE - INCOME + DESPESA + RENDIMENTO - Connected to Google Drive - Unable to connect to Google Drive + Conectado ao Google Drive + Impossível conectar ao Google Drive Please enter an amount to split - external service - Updated transaction recurring schedule - Since - All time - Recommend in Play Store - until %1$s + serviço externo + Agenda recorrente de transação atualizada + Desde + Desde o início + Recomendado na Play Store + até %1$s on %1$s - for %1$s times - Compact View + por %1$s vezes + Visualização compacta Book %1$d diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 329c7ff77..394812e63 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -20,9 +20,9 @@ Editar Conta Informação Exportar - Adicionar nova transação a uma conta + Adicionar nova transacção a uma conta Sem contas para mostrar - Nome da Conta + Nome da conta Cancelar Gravar Testar @@ -69,35 +69,35 @@ Enviar para… Mover - Move %1$d transação - Conta Destino - Aceder ao SD Card - Impossível mover transação.\nA conta destino usa uma moeda diferente da conta de origem. + Mover %1$d transacções + Conta destino + Aceder ao cartão SD + Impossível mover transacção.\nA conta destino usa uma moeda diferente da conta origem Geral - Sobre - Escolhe a moeda padrão + Acerca + Escolher a moeda padrão Moeda padrão Moeda padrão para as novas contas - Permite gravar transações no GnuCash para Android + Permite gravar transacções no GnuCash para Android Permite criar contas no GnuCash para Android Os seus dados Gnucash Ler e modificar dados do GnuCash - Gravar transações no GnuCash + Gravar transacções no GnuCash Criar contas no GnuCash Mostrar conta - Criar Contas + Criar contas Escolha as contas a criar Não existem contas no GnuCash.\nCrie uma conta antes de adicionar um widget Versão - Licensa + Licença Apache License v2.0. Clique para obter detalhes Preferências Escolha uma conta Não existem transacções para exportar - Palavra passe - Preferências de palavra passe - Palavra passe ligada - Palavra passe desligada + Senha + Preferências de senha + Senha ligada + Senha desligada Mudar Palavra Passe Sobre o GnuCash Um programa de registo e gestão de finanças pessoais para Android diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 057c42715..1d76ac7ae 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -418,7 +418,7 @@ Последний квартал Последнее полугодие Последний год - Всё время + За всё время Другой интервал… diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 0c1a4909f..892de105d 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -16,44 +16,44 @@ limitations under the License. --> - Create Account - Edit Account - Info - Export… - Add a new transaction to an account - No accounts to display - Account name - Cancel - Save + Skapa konto + Redigera konto + Information + Exportera… + Lägg till en transaktion till ett konto + Det finns inga konton att visa + Kontonamn + Avbryt + Spara Test - Enter Passcode - Wrong passcode, please try again - Passcode set - Please confirm your passcode - Invalid passcode confirmation. Please try again - Description - Amount - New transaction - No transactions to display - DATE & TIME - Account + Ange pinkod + Fel pinkod, var god försök igen + Pinkod sparad + Var god bekräfta din pinkod + Felaktig pinkod. Var god försök igen + Beskrivning + Belopp + Lägg till transaktion + Det finns inga transaktioner att visa + DATUM & TID + Konto DEBIT - CREDIT - Accounts - Transactions - Delete - Delete - Cancel - Account deleted - Confirm delete - All transactions in this account will also be deleted - Edit Transaction - Add note - MOVE - %1$d selected - Balance: - Export To: - Export Transactions + KREDIT + Konton + Transaktioner + Ta bort + Ta bort + Avbryt + Kontot borttaget + Bekräfta radering + Alla transaktioner i kontot kommer också raderas + Redigera transaktion + Lägg till anteckning + FLYTTA + %1$d har markerats + Saldo: + Exportera till: + Exportera transaktioner Export all transactions By default, only new transactions since last export will be exported. Check this option to export all transactions Error exporting %1$s file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e2b73fd7e..66b15cc70 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -63,7 +63,7 @@ 设置 SD卡 - Dropbox + DropBox Google 云端硬盘 ownCloud 发送到... @@ -461,10 +461,10 @@ 计划交易已经更新 时间点: 全部 - 在Paly Store上推荐 + 在 Play Store 上推荐 直到%1$s 在%1$s 共%1$s次 紧凑视图 - Book %1$d + 账簿 %1$d diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 206a96857..058c5613e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -486,4 +486,5 @@ for %1$s times Compact View Book %1$d + never diff --git a/app/src/test/java/org/gnucash/android/test/unit/export/BackupTest.java b/app/src/test/java/org/gnucash/android/test/unit/export/BackupTest.java index 712d515c1..7c837a403 100644 --- a/app/src/test/java/org/gnucash/android/test/unit/export/BackupTest.java +++ b/app/src/test/java/org/gnucash/android/test/unit/export/BackupTest.java @@ -62,7 +62,7 @@ public void shouldCreateBackup(){ } @Test - public void shouldCreateBackupFileName(){ + public void shouldCreateBackupFileName() throws Exporter.ExporterException { Exporter exporter = new GncXmlExporter(new ExportParams(ExportFormat.XML)); List xmlFiles = exporter.generateExport(); diff --git a/app/src/test/java/org/gnucash/android/test/unit/service/ScheduledActionServiceTest.java b/app/src/test/java/org/gnucash/android/test/unit/service/ScheduledActionServiceTest.java index 4b8dfda77..ac1db752e 100644 --- a/app/src/test/java/org/gnucash/android/test/unit/service/ScheduledActionServiceTest.java +++ b/app/src/test/java/org/gnucash/android/test/unit/service/ScheduledActionServiceTest.java @@ -15,12 +15,13 @@ */ package org.gnucash.android.test.unit.service; +import android.content.ContentValues; import android.database.sqlite.SQLiteDatabase; -import android.support.annotation.NonNull; import org.gnucash.android.BuildConfig; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.adapter.BooksDbAdapter; import org.gnucash.android.db.adapter.CommoditiesDbAdapter; @@ -39,10 +40,12 @@ import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; +import org.gnucash.android.model.TransactionType; import org.gnucash.android.service.ScheduledActionService; import org.gnucash.android.test.unit.testutil.GnucashTestRunner; import org.gnucash.android.test.unit.testutil.ShadowCrashlytics; import org.gnucash.android.test.unit.testutil.ShadowUserVoice; +import org.gnucash.android.util.TimestampHelper; import org.joda.time.DateTime; import org.joda.time.LocalDateTime; import org.joda.time.Weeks; @@ -57,6 +60,7 @@ import java.io.File; import java.io.IOException; import java.math.BigDecimal; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; @@ -79,6 +83,7 @@ public class ScheduledActionServiceTest { private static Account mTransferAccount = new Account("Transfer Account"); private static Transaction mTemplateTransaction; + private TransactionsDbAdapter mTransactionsDbAdapter; public void createAccounts(){ try { @@ -117,9 +122,8 @@ public void setUp(){ accountsDbAdapter.addRecord(mBaseAccount); accountsDbAdapter.addRecord(mTransferAccount); - TransactionsDbAdapter transactionsDbAdapter = TransactionsDbAdapter.getInstance(); - transactionsDbAdapter.addRecord(mTemplateTransaction, DatabaseAdapter.UpdateMethod.insert); - + mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); + mTransactionsDbAdapter.addRecord(mTemplateTransaction, DatabaseAdapter.UpdateMethod.insert); } @Test @@ -290,9 +294,6 @@ public void recurringTransactions_shouldHaveScheduledActionUID(){ * was done on Monday and it's Thursday, two backups have been * missed. Doing the two missed backups plus today's wouldn't be * useful, so just one should be done.

- * - *

Note: the execution count will include the missed runs - * as computeNextCountBasedScheduledExecutionTime depends on it.

*/ @Test public void scheduledBackups_shouldRunOnlyOnce(){ @@ -302,6 +303,7 @@ public void scheduledBackups_shouldRunOnlyOnce(){ scheduledBackup.setRecurrence(PeriodType.MONTH, 1); scheduledBackup.setExecutionCount(2); scheduledBackup.setLastRun(LocalDateTime.now().minusMonths(2).toDate().getTime()); + long previousLastRun = scheduledBackup.getLastRunTime(); ExportParams backupParams = new ExportParams(ExportFormat.XML); backupParams.setExportTarget(ExportParams.ExportTarget.SD_CARD); @@ -317,13 +319,16 @@ public void scheduledBackups_shouldRunOnlyOnce(){ // Check there's not a backup for each missed run ScheduledActionService.processScheduledActions(actions, mDb); assertThat(scheduledBackup.getExecutionCount()).isEqualTo(3); + assertThat(scheduledBackup.getLastRunTime()).isGreaterThan(previousLastRun); File[] backupFiles = backupFolder.listFiles(); assertThat(backupFiles).hasSize(1); assertThat(backupFiles[0]).exists().hasExtension("gnca"); // Check also across service runs + previousLastRun = scheduledBackup.getLastRunTime(); ScheduledActionService.processScheduledActions(actions, mDb); assertThat(scheduledBackup.getExecutionCount()).isEqualTo(3); + assertThat(scheduledBackup.getLastRunTime()).isEqualTo(previousLastRun); backupFiles = backupFolder.listFiles(); assertThat(backupFiles).hasSize(1); assertThat(backupFiles[0]).exists().hasExtension("gnca"); @@ -340,6 +345,7 @@ public void scheduledBackups_shouldNotRunBeforeNextScheduledExecution(){ ScheduledAction scheduledBackup = new ScheduledAction(ScheduledAction.ActionType.BACKUP); scheduledBackup.setStartTime(LocalDateTime.now().minusDays(2).toDate().getTime()); scheduledBackup.setLastRun(scheduledBackup.getStartTime()); + long previousLastRun = scheduledBackup.getLastRunTime(); scheduledBackup.setExecutionCount(1); scheduledBackup.setRecurrence(PeriodType.WEEK, 1); @@ -357,9 +363,107 @@ public void scheduledBackups_shouldNotRunBeforeNextScheduledExecution(){ ScheduledActionService.processScheduledActions(actions, mDb); assertThat(scheduledBackup.getExecutionCount()).isEqualTo(1); + assertThat(scheduledBackup.getLastRunTime()).isEqualTo(previousLastRun); + assertThat(backupFolder.listFiles()).hasSize(0); + } + + /** + * Tests that an scheduled backup doesn't include transactions added or modified + * previous to the last run. + */ + @Test + public void scheduledBackups_shouldNotIncludeTransactionsPreviousToTheLastRun() { + ScheduledAction scheduledBackup = new ScheduledAction(ScheduledAction.ActionType.BACKUP); + scheduledBackup.setStartTime(LocalDateTime.now().minusDays(15).toDate().getTime()); + scheduledBackup.setLastRun(LocalDateTime.now().minusDays(8).toDate().getTime()); + long previousLastRun = scheduledBackup.getLastRunTime(); + scheduledBackup.setExecutionCount(1); + scheduledBackup.setRecurrence(PeriodType.WEEK, 1); + ExportParams backupParams = new ExportParams(ExportFormat.QIF); + backupParams.setExportTarget(ExportParams.ExportTarget.SD_CARD); + backupParams.setExportStartTime(new Timestamp(scheduledBackup.getStartTime())); + scheduledBackup.setTag(backupParams.toCsv()); + + // Create a transaction with a modified date previous to the last run + Transaction transaction = new Transaction("Tandoori express"); + Split split = new Split(new Money("10", Commodity.DEFAULT_COMMODITY.getCurrencyCode()), + mBaseAccount.getUID()); + split.setType(TransactionType.DEBIT); + transaction.addSplit(split); + transaction.addSplit(split.createPair(mTransferAccount.getUID())); + mTransactionsDbAdapter.addRecord(transaction); + // We set the date directly in the database as the corresponding field + // is ignored when the object is stored. It's set through a trigger instead. + setTransactionInDbModifiedTimestamp(transaction.getUID(), + new Timestamp(LocalDateTime.now().minusDays(9).toDate().getTime())); + + File backupFolder = new File( + Exporter.getExportFolderPath(BooksDbAdapter.getInstance().getActiveBookUID())); + assertThat(backupFolder).exists(); + assertThat(backupFolder.listFiles()).isEmpty(); + + List actions = new ArrayList<>(); + actions.add(scheduledBackup); + ScheduledActionService.processScheduledActions(actions, mDb); + + assertThat(scheduledBackup.getExecutionCount()).isEqualTo(2); + assertThat(scheduledBackup.getLastRunTime()).isGreaterThan(previousLastRun); assertThat(backupFolder.listFiles()).hasSize(0); } + /** + * Sets the transaction modified timestamp directly in the database. + * + * @param transactionUID UID of the transaction to set the modified timestamp. + * @param timestamp new modified timestamp. + */ + private void setTransactionInDbModifiedTimestamp(String transactionUID, Timestamp timestamp) { + ContentValues values = new ContentValues(); + values.put(DatabaseSchema.TransactionEntry.COLUMN_MODIFIED_AT, + TimestampHelper.getUtcStringFromTimestamp(timestamp)); + mTransactionsDbAdapter.updateTransaction(values, "uid = ?", + new String[]{transactionUID}); + } + + /** + * Tests that an scheduled backup includes transactions added or modified + * after the last run. + */ + @Test + public void scheduledBackups_shouldIncludeTransactionsAfterTheLastRun() { + ScheduledAction scheduledBackup = new ScheduledAction(ScheduledAction.ActionType.BACKUP); + scheduledBackup.setStartTime(LocalDateTime.now().minusDays(15).toDate().getTime()); + scheduledBackup.setLastRun(LocalDateTime.now().minusDays(8).toDate().getTime()); + long previousLastRun = scheduledBackup.getLastRunTime(); + scheduledBackup.setExecutionCount(1); + scheduledBackup.setRecurrence(PeriodType.WEEK, 1); + ExportParams backupParams = new ExportParams(ExportFormat.QIF); + backupParams.setExportTarget(ExportParams.ExportTarget.SD_CARD); + backupParams.setExportStartTime(new Timestamp(scheduledBackup.getStartTime())); + scheduledBackup.setTag(backupParams.toCsv()); + + Transaction transaction = new Transaction("Orient palace"); + Split split = new Split(new Money("10", Commodity.DEFAULT_COMMODITY.getCurrencyCode()), + mBaseAccount.getUID()); + split.setType(TransactionType.DEBIT); + transaction.addSplit(split); + transaction.addSplit(split.createPair(mTransferAccount.getUID())); + mTransactionsDbAdapter.addRecord(transaction); + + File backupFolder = new File( + Exporter.getExportFolderPath(BooksDbAdapter.getInstance().getActiveBookUID())); + assertThat(backupFolder).exists(); + assertThat(backupFolder.listFiles()).isEmpty(); + + List actions = new ArrayList<>(); + actions.add(scheduledBackup); + ScheduledActionService.processScheduledActions(actions, mDb); + + assertThat(scheduledBackup.getExecutionCount()).isEqualTo(2); + assertThat(scheduledBackup.getLastRunTime()).isGreaterThan(previousLastRun); + assertThat(backupFolder.listFiles()).hasSize(1); + } + @After public void tearDown(){ TransactionsDbAdapter.getInstance().deleteAllRecords();