diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cf2c62df..d2e9a7e8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ Change Log =============================================================================== +Version 2.1.3 *(2016-10-20)* +---------------------------- +* Fixed: Scheduled exports execute too often or not at all in some cases +* Fixed: Crash if device is rotated during first-run wizard execution +* Fixed: Negative values displayed as green on homescreen widget +* Improved: Homescreen widget now allows to select the book to use +* Improved: Update Russian translation + Version 2.1.2 *(2016-09-21)* ---------------------------- * Fixed: Scheduled exports always run daily (no matter the actual schedule) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0ce07a1d1..48c117f61 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -30,5 +30,11 @@ The following (incomplete list of) people (in no particular order) contributed ( * Terry Chung * Caesar Wirth * Alceu Rodrigues Neto +* Carlo Zancanaro +* Eric Daly +* Weslly Oliveira +* Felipe Morato +* Alceu Rodrigues Neto +* Salama AB 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 1d5200fad..6b520971a 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 = 2 -def versionBuild = 0 +def versionPatch = 3 +def versionBuild = 1 def buildTime() { def df = new SimpleDateFormat("yyyyMMdd HH:mm 'UTC'") diff --git a/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java b/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java index 859e87b0b..b8327546b 100644 --- a/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java +++ b/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java @@ -126,7 +126,7 @@ public void onCreate(){ BookDbHelper bookDbHelper = new BookDbHelper(getApplicationContext()); mBooksDbAdapter = new BooksDbAdapter(bookDbHelper.getWritableDatabase()); - initDatabaseAdapters(); + initializeDatabaseAdapters(); setDefaultCurrencyCode(getDefaultCurrencyCode()); if (BuildConfig.DEBUG && !isRoboUnitTest()) @@ -137,7 +137,7 @@ public void onCreate(){ * Initialize database adapter singletons for use in the application * This method should be called every time a new book is opened */ - private static void initDatabaseAdapters() { + private static void initializeDatabaseAdapters() { if (mDbHelper != null){ //close if open mDbHelper.getReadableDatabase().close(); } @@ -205,15 +205,23 @@ public static BooksDbAdapter getBooksDbAdapter(){ } /** - * Loads the book with GUID {@code bookUID} + * Loads the book with GUID {@code bookUID} and opens the AccountsActivity * @param bookUID GUID of the book to be loaded */ public static void loadBook(@NonNull String bookUID){ - mBooksDbAdapter.setActive(bookUID); - initDatabaseAdapters(); + activateBook(bookUID); AccountsActivity.start(getAppContext()); } + /** + * Activates the book with unique identifer {@code bookUID}, and refreshes the database adapters + * @param bookUID GUID of the book to be activated + */ + public static void activateBook(@NonNull String bookUID){ + mBooksDbAdapter.setActive(bookUID); + initializeDatabaseAdapters(); + } + /** * Returns the currently active database in the application * @return Currently active {@link SQLiteDatabase} diff --git a/app/src/main/java/org/gnucash/android/db/BookDbHelper.java b/app/src/main/java/org/gnucash/android/db/BookDbHelper.java index 87adc17d2..6fa1a6ebb 100644 --- a/app/src/main/java/org/gnucash/android/db/BookDbHelper.java +++ b/app/src/main/java/org/gnucash/android/db/BookDbHelper.java @@ -124,6 +124,16 @@ public void onCreate(SQLiteDatabase db) { } + /** + * Returns the database for the book + * @param bookUID GUID of the book + * @return SQLiteDatabase of the book + */ + public static SQLiteDatabase getDatabase(String bookUID){ + DatabaseHelper dbHelper = new DatabaseHelper(GnuCashApplication.getAppContext(), bookUID); + return dbHelper.getWritableDatabase(); + } + /** * Inserts the book into the database * @param db Book database diff --git a/app/src/main/java/org/gnucash/android/db/adapter/BooksDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/BooksDbAdapter.java index 65e06c950..abbce8027 100644 --- a/app/src/main/java/org/gnucash/android/db/adapter/BooksDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/BooksDbAdapter.java @@ -31,6 +31,8 @@ import org.gnucash.android.ui.settings.PreferenceActivity; import org.gnucash.android.util.TimestampHelper; +import java.util.List; + /** * Database adapter for creating/modifying book entries */ diff --git a/app/src/main/java/org/gnucash/android/model/ScheduledAction.java b/app/src/main/java/org/gnucash/android/model/ScheduledAction.java index f7279af5a..ff27fc76d 100644 --- a/app/src/main/java/org/gnucash/android/model/ScheduledAction.java +++ b/app/src/main/java/org/gnucash/android/model/ScheduledAction.java @@ -173,35 +173,64 @@ public long getTimeOfLastSchedule(){ } /** - * Computes the next time that this scheduled action is supposed to be executed + * Computes the next time that this scheduled action is supposed to be + * executed based on the execution count. + * *

This method does not consider the end time, or number of times it should be run. - * It only considers when the next execution would theoretically be due

+ * It only considers when the next execution would theoretically be due.

+ * * @return Next run time in milliseconds */ - public long computeNextScheduledExecutionTime(){ - int multiplier = mRecurrence.getPeriodType().getMultiplier(); - //this is the last planned time for the action to occur, not the last run time - long lastActionTime = getTimeOfLastSchedule(); //mStartDate + ((mExecutionCount-1)*getPeriod()); - if (lastActionTime < 0){ + public long computeNextCountBasedScheduledExecutionTime(){ + return computeNextScheduledExecutionTimeStartingAt(getTimeOfLastSchedule()); + } + + /** + * Computes the next time that this scheduled action is supposed to be + * executed based on the time of the last run. + * + *

This method does not consider the end time, or number of times it should be run. + * It only considers when the next execution would theoretically be due.

+ * + * @return Next run time in milliseconds + */ + public long computeNextTimeBasedScheduledExecutionTime() { + return computeNextScheduledExecutionTimeStartingAt(getLastRunTime()); + } + + /** + * Computes the next time that this scheduled action is supposed to be + * executed starting at startTime. + * + *

This method does not consider the end time, or number of times it should be run. + * It only considers when the next execution would theoretically be due.

+ * + * @param startTime time in milliseconds to use as start to compute the next schedule. + * + * @return Next run time in milliseconds + */ + private long computeNextScheduledExecutionTimeStartingAt(long startTime) { + if (startTime <= 0){ // has never been run return mStartDate; } - LocalDateTime localDate = LocalDateTime.fromDateFields(new Date(lastActionTime)); + int multiplier = mRecurrence.getPeriodType().getMultiplier(); + LocalDateTime nextScheduledExecution = LocalDateTime.fromDateFields(new Date(startTime)); switch (mRecurrence.getPeriodType()) { case DAY: - localDate = localDate.plusDays(multiplier); + nextScheduledExecution = nextScheduledExecution.plusDays(multiplier); break; case WEEK: - localDate = localDate.plusWeeks(multiplier); + nextScheduledExecution = nextScheduledExecution.plusWeeks(multiplier); break; case MONTH: - localDate = localDate.plusMonths(multiplier); + nextScheduledExecution = nextScheduledExecution.plusMonths(multiplier); break; case YEAR: - localDate = localDate.plusYears(multiplier); + nextScheduledExecution = nextScheduledExecution.plusYears(multiplier); break; } - return localDate.toDate().getTime(); + return nextScheduledExecution.toDate().getTime(); } /** diff --git a/app/src/main/java/org/gnucash/android/receivers/TransactionAppWidgetProvider.java b/app/src/main/java/org/gnucash/android/receivers/TransactionAppWidgetProvider.java index 053c2a0f2..2f6e4d81c 100644 --- a/app/src/main/java/org/gnucash/android/receivers/TransactionAppWidgetProvider.java +++ b/app/src/main/java/org/gnucash/android/receivers/TransactionAppWidgetProvider.java @@ -21,8 +21,11 @@ import android.content.SharedPreferences.Editor; import android.preference.PreferenceManager; +import org.gnucash.android.db.adapter.BooksDbAdapter; +import org.gnucash.android.model.Book; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; +import org.gnucash.android.ui.settings.PreferenceActivity; /** * {@link AppWidgetProvider} which is responsible for managing widgets on the homescreen @@ -43,13 +46,13 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, for (int i=0; i 0 && endTime < now) - return executionCount; - - if (scheduledAction.computeNextScheduledExecutionTime() > now) + if (!shouldExecuteScheduledBackup(scheduledAction)) return 0; ExportParams params = ExportParams.parseCsv(scheduledAction.getTag()); try { //wait for async task to finish before we proceed (we are holding a wake lock) new ExportAsyncTask(GnuCashApplication.getAppContext(), db).execute(params).get(); - scheduledAction.setExecutionCount(++executionCount); } catch (InterruptedException | ExecutionException e) { Crashlytics.logException(e); Log.e(LOG_TAG, e.getMessage()); } - return executionCount; + return 1; + } + + private static boolean shouldExecuteScheduledBackup(ScheduledAction scheduledAction) { + long now = System.currentTimeMillis(); + long endTime = scheduledAction.getEndTime(); + + if (endTime > 0 && endTime < now) + return false; + + if (scheduledAction.computeNextTimeBasedScheduledExecutionTime() > now) + return false; + + return true; } /** @@ -214,7 +221,7 @@ private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDa //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.computeNextScheduledExecutionTime(); + long transactionTime = scheduledAction.computeNextCountBasedScheduledExecutionTime(); while (transactionTime <= endTime) { Transaction recurringTrxn = new Transaction(trxnTemplate, true); recurringTrxn.setTime(transactionTime); @@ -224,7 +231,7 @@ private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDa if (totalPlannedExecutions > 0 && executionCount >= totalPlannedExecutions) break; //if we hit the total planned executions set, then abort - transactionTime = scheduledAction.computeNextScheduledExecutionTime(); + transactionTime = scheduledAction.computeNextCountBasedScheduledExecutionTime(); } transactionsDbAdapter.bulkAddRecords(transactions, DatabaseAdapter.UpdateMethod.insert); diff --git a/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java b/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java index eee5966c3..89fa315a0 100644 --- a/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java @@ -102,6 +102,12 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(getContentView()); + //if a parameter was passed to open an account within a specific book, then switch + String bookUID = getIntent().getStringExtra(UxArgument.BOOK_UID); + if (bookUID != null && !bookUID.equals(BooksDbAdapter.getInstance().getActiveBookUID())){ + GnuCashApplication.activateBook(bookUID); + } + ButterKnife.bind(this); setSupportActionBar(mToolbar); final ActionBar actionBar = getSupportActionBar(); diff --git a/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java b/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java index e078ba187..6d777f5f4 100644 --- a/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java @@ -29,6 +29,7 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.BooksDbAdapter; import org.gnucash.android.ui.account.AccountFormFragment; import org.gnucash.android.ui.budget.BudgetAmountEditorFragment; import org.gnucash.android.ui.budget.BudgetFormFragment; @@ -57,6 +58,12 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_form); + //if a parameter was passed to open an account within a specific book, then switch + String bookUID = getIntent().getStringExtra(UxArgument.BOOK_UID); + if (bookUID != null && !bookUID.equals(BooksDbAdapter.getInstance().getActiveBookUID())){ + GnuCashApplication.activateBook(bookUID); + } + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); diff --git a/app/src/main/java/org/gnucash/android/ui/common/UxArgument.java b/app/src/main/java/org/gnucash/android/ui/common/UxArgument.java index b2077c620..9a4a07333 100644 --- a/app/src/main/java/org/gnucash/android/ui/common/UxArgument.java +++ b/app/src/main/java/org/gnucash/android/ui/common/UxArgument.java @@ -102,6 +102,11 @@ public final class UxArgument { */ public static final String BUDGET_AMOUNT_LIST = "budget_amount_list"; + /** + * GUID of a book which is relevant for a specific action + */ + public static final String BOOK_UID = "book_uid"; + //prevent initialization of instances of this class private UxArgument(){ //prevent even the native class from calling the ctor diff --git a/app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java b/app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java index eb13294f8..76e301587 100644 --- a/app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java @@ -25,29 +25,39 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.v4.widget.SimpleCursorAdapter; import android.util.Log; import android.view.View; +import android.widget.AdapterView; import android.widget.Button; import android.widget.RemoteViews; import android.widget.Spinner; import android.widget.Toast; import org.gnucash.android.R; +import org.gnucash.android.db.BookDbHelper; +import org.gnucash.android.db.DatabaseHelper; +import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.BooksDbAdapter; import org.gnucash.android.model.Account; +import org.gnucash.android.model.Book; import org.gnucash.android.model.Money; import org.gnucash.android.receivers.TransactionAppWidgetProvider; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.common.FormActivity; import org.gnucash.android.ui.common.UxArgument; +import org.gnucash.android.ui.settings.PreferenceActivity; import org.gnucash.android.ui.transaction.TransactionsActivity; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; import java.util.Locale; +import butterknife.Bind; +import butterknife.ButterKnife; + /** * Activity for configuration which account to display on a widget. * The activity is opened each time a widget is added to the homescreen @@ -57,19 +67,42 @@ public class WidgetConfigurationActivity extends Activity { private AccountsDbAdapter mAccountsDbAdapter; private int mAppWidgetId; - private Spinner mAccountsSpinner; - private Button mOkButton; - private Button mCancelButton; - + @Bind(R.id.input_accounts_spinner) Spinner mAccountsSpinner; + @Bind(R.id.input_books_spinner) Spinner mBooksSpinner; + + @Bind(R.id.btn_save) Button mOkButton; + @Bind(R.id.btn_cancel) Button mCancelButton; + private SimpleCursorAdapter mAccountsCursorAdapter; + + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.widget_configuration); setResult(RESULT_CANCELED); - - mAccountsSpinner = (Spinner) findViewById(R.id.input_accounts_spinner); - mOkButton = (Button) findViewById(R.id.btn_save); - mCancelButton = (Button) findViewById(R.id.btn_cancel); + + ButterKnife.bind(this); + + BooksDbAdapter booksDbAdapter = BooksDbAdapter.getInstance(); + Cursor booksCursor = booksDbAdapter.fetchAllRecords(); + String currentBookUID = booksDbAdapter.getActiveBookUID(); + + //determine the position of the currently active book in the cursor + int position = 0; + while (booksCursor.moveToNext()){ + String bookUID = booksCursor.getString(booksCursor.getColumnIndexOrThrow(DatabaseSchema.BookEntry.COLUMN_UID)); + if (bookUID.equals(currentBookUID)) + break; + ++position; + } + + SimpleCursorAdapter booksCursorAdapter = new SimpleCursorAdapter(this, + android.R.layout.simple_spinner_item, booksCursor, + new String[]{DatabaseSchema.BookEntry.COLUMN_DISPLAY_NAME}, + new int[]{android.R.id.text1}, 0); + booksCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mBooksSpinner.setAdapter(booksCursorAdapter); + mBooksSpinner.setSelection(position); mAccountsDbAdapter = AccountsDbAdapter.getInstance(); Cursor cursor = mAccountsDbAdapter.fetchAllRecordsOrderedByFullName(); @@ -79,10 +112,10 @@ public void onCreate(Bundle savedInstanceState) { finish(); } - SimpleCursorAdapter cursorAdapter = new QualifiedAccountNameCursorAdapter(this, cursor); + mAccountsCursorAdapter = new QualifiedAccountNameCursorAdapter(this, cursor); //without this line, the app crashes when a user tries to select an account - cursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - mAccountsSpinner.setAdapter(cursorAdapter); + mAccountsCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mAccountsSpinner.setAdapter(mAccountsCursorAdapter); bindListeners(); } @@ -91,6 +124,24 @@ public void onCreate(Bundle savedInstanceState) { * Sets click listeners for the buttons in the dialog */ private void bindListeners() { + mBooksSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + Book book = BooksDbAdapter.getInstance().getRecord(id); + SQLiteDatabase db = new DatabaseHelper(WidgetConfigurationActivity.this, book.getUID()).getWritableDatabase(); + mAccountsDbAdapter = new AccountsDbAdapter(db); + + Cursor cursor = mAccountsDbAdapter.fetchAllRecordsOrderedByFullName(); + mAccountsCursorAdapter.swapCursor(cursor); + mAccountsCursorAdapter.notifyDataSetChanged(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + //nothing to see here, move along + } + }); + mOkButton.setOnClickListener(new View.OnClickListener() { @Override @@ -110,12 +161,17 @@ public void onClick(View v) { long accountId = mAccountsSpinner.getSelectedItemId(); String accountUID = mAccountsDbAdapter.getUID(accountId); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(WidgetConfigurationActivity.this); + + long bookId = mBooksSpinner.getSelectedItemId(); + String bookUID = BooksDbAdapter.getInstance().getUID(bookId); + + SharedPreferences prefs = PreferenceActivity.getBookSharedPreferences(bookUID); + //PreferenceManager.getDefaultSharedPreferences(WidgetConfigurationActivity.this); Editor editor = prefs.edit(); editor.putString(UxArgument.SELECTED_ACCOUNT_UID + mAppWidgetId, accountUID); - editor.commit(); + editor.apply(); - updateWidget(WidgetConfigurationActivity.this, mAppWidgetId, accountUID); + updateWidget(WidgetConfigurationActivity.this, mAppWidgetId, accountUID, bookUID); Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); @@ -137,14 +193,16 @@ public void onClick(View v) { * Updates the widget with id appWidgetId with information from the * account with record ID accountId * If the account has been deleted, then a notice is posted in the widget - * @param appWidgetId ID of the widget to be updated + * @param appWidgetId ID of the widget to be updated * @param accountUID GUID of the account tied to the widget + * @param bookUID GUID of the book with the relevant account */ - public static void updateWidget(final Context context, int appWidgetId, String accountUID) { + public static void updateWidget(final Context context, int appWidgetId, String accountUID, String bookUID) { Log.i("WidgetConfiguration", "Updating widget: " + appWidgetId); AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); - AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); + AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(BookDbHelper.getDatabase(bookUID)); + final Account account; try { account = accountsDbAdapter.getRecord(accountUID); @@ -161,9 +219,9 @@ public static void updateWidget(final Context context, int appWidgetId, String a views.setOnClickPendingIntent(R.id.widget_layout, pendingIntent); views.setOnClickPendingIntent(R.id.btn_new_transaction, pendingIntent); appWidgetManager.updateAppWidget(appWidgetId, views); - Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); + Editor editor = PreferenceActivity.getActiveBookSharedPreferences().edit(); //PreferenceManager.getDefaultSharedPreferences(context).edit(); editor.remove(UxArgument.SELECTED_ACCOUNT_UID + appWidgetId); - editor.commit(); + editor.apply(); return; } @@ -175,7 +233,7 @@ public static void updateWidget(final Context context, int appWidgetId, String a views.setTextViewText(R.id.transactions_summary, accountBalance.formattedString(Locale.getDefault())); - int color = account.getBalance().isNegative() ? R.color.debit_red : R.color.credit_green; + int color = accountBalance.isNegative() ? R.color.debit_red : R.color.credit_green; views.setTextColor(R.id.transactions_summary, context.getResources().getColor(color)); @@ -183,6 +241,7 @@ public static void updateWidget(final Context context, int appWidgetId, String a accountViewIntent.setAction(Intent.ACTION_VIEW); accountViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); accountViewIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, accountUID); + accountViewIntent.putExtra(UxArgument.BOOK_UID, bookUID); PendingIntent accountPendingIntent = PendingIntent .getActivity(context, appWidgetId, accountViewIntent, 0); views.setOnClickPendingIntent(R.id.widget_layout, accountPendingIntent); @@ -191,6 +250,7 @@ public static void updateWidget(final Context context, int appWidgetId, String a newTransactionIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); newTransactionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); newTransactionIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.TRANSACTION.name()); + newTransactionIntent.putExtra(UxArgument.BOOK_UID, bookUID); newTransactionIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, accountUID); PendingIntent pendingIntent = PendingIntent .getActivity(context, appWidgetId, newTransactionIntent, 0); @@ -212,7 +272,8 @@ public static void updateAllWidgets(final Context context){ //update widgets asynchronously so as not to block method which called the update //inside the computation of the account balance new Thread(new Runnable() { - SharedPreferences defaultSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences defaultSharedPrefs = PreferenceActivity.getActiveBookSharedPreferences(); + //PreferenceManager.getDefaultSharedPreferences(context); @Override public void run() { @@ -223,7 +284,7 @@ public void run() { if (accountUID == null) continue; - updateWidget(context, widgetId, accountUID); + updateWidget(context, widgetId, accountUID, BooksDbAdapter.getInstance().getActiveBookUID()); } } }).start(); 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 6d7e391ad..ff7fa0e67 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 @@ -160,7 +160,7 @@ 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().toString()); + lastSyncText.setText(PreferencesHelper.getLastExportTime(bookUID).toString()); TextView labelLastSync = (TextView) view.findViewById(R.id.label_last_sync); labelLastSync.setText(R.string.label_last_export_time); diff --git a/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardActivity.java b/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardActivity.java index 7254ef5f9..9284990e3 100644 --- a/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardActivity.java @@ -83,19 +83,17 @@ public class FirstRunWizardActivity extends AppCompatActivity implements public void onCreate(Bundle savedInstanceState) { + // we need to construct the wizard model before we call super.onCreate, because it's used in + // onGetPage (which is indirectly called through super.onCreate if savedInstanceState is not + // null) + mWizardModel = createWizardModel(savedInstanceState); + super.onCreate(savedInstanceState); setContentView(R.layout.activity_first_run_wizard); ButterKnife.bind(this); setTitle(getString(R.string.title_setup_gnucash)); - mWizardModel = new FirstRunWizardModel(this); - if (savedInstanceState != null) { - mWizardModel.load(savedInstanceState.getBundle("model")); - } - - mWizardModel.registerListener(this); - mPagerAdapter = new MyPagerAdapter(getSupportFragmentManager()); mPager.setAdapter(mPagerAdapter); mStepPagerStrip @@ -197,6 +195,24 @@ public void onClick(View view) { updateBottomBar(); } + /** + * Create the wizard model for the activity, taking into accoun the savedInstanceState if it + * exists (and if it contains a "model" key that we can use). + * @param savedInstanceState the instance state available in {{@link #onCreate(Bundle)}} + * @return an appropriate wizard model for this activity + */ + private AbstractWizardModel createWizardModel(Bundle savedInstanceState) { + AbstractWizardModel model = new FirstRunWizardModel(this); + if (savedInstanceState != null) { + Bundle wizardModel = savedInstanceState.getBundle("model"); + if (wizardModel != null) { + model.load(wizardModel); + } + } + model.registerListener(this); + return model; + } + /** * Create accounts depending on the user preference (import or default set) and finish this activity *

This method also removes the first run flag from the application

diff --git a/app/src/main/java/org/gnucash/android/util/PreferencesHelper.java b/app/src/main/java/org/gnucash/android/util/PreferencesHelper.java index d58f2e7dc..27be8b7bd 100644 --- a/app/src/main/java/org/gnucash/android/util/PreferencesHelper.java +++ b/app/src/main/java/org/gnucash/android/util/PreferencesHelper.java @@ -84,4 +84,20 @@ public static Timestamp getLastExportTime() { Log.d(LOG_TAG, "Retrieving '" + utcString + "' as lastExportTime from Android Preferences."); return TimestampHelper.getTimestampFromUtcString(utcString); } + + /** + * Get the time for the last export operation of a specific book. + * + * @return A {@link Timestamp} with the time. + */ + public static Timestamp getLastExportTime(String bookUID) { + final String utcString = + GnuCashApplication.getAppContext() + .getSharedPreferences(bookUID, Context.MODE_PRIVATE) + .getString(PREFERENCE_LAST_EXPORT_TIME_KEY, + TimestampHelper.getUtcStringFromTimestamp( + TimestampHelper.getTimestampFromEpochZero())); + Log.d(LOG_TAG, "Retrieving '" + utcString + "' as lastExportTime from Android Preferences."); + return TimestampHelper.getTimestampFromUtcString(utcString); + } } \ No newline at end of file diff --git a/app/src/main/res/layout/widget_configuration.xml b/app/src/main/res/layout/widget_configuration.xml index 063392010..f9ef7c19f 100644 --- a/app/src/main/res/layout/widget_configuration.xml +++ b/app/src/main/res/layout/widget_configuration.xml @@ -22,18 +22,33 @@ android:orientation="vertical" > + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 75f12c45e..057c42715 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -17,10 +17,10 @@ --> Создать счёт - Редактировать счёт + Изменить счёт Информация - Экспортировать OFX - Добавить к счёту новую проводку + Экспорт… + Новая проводка Нет счетов Имя счёта Отмена @@ -41,24 +41,24 @@ КРЕДИТ Счета Проводки - УДАЛИТЬ + Удалить Удалить Отмена Счёт удалён Подтвердите удаление Все проводки этого счёта будут удалены Редактировать проводку - Замечание + Заметка ПЕРЕНЕСТИ %1$d выбрано Баланс: - Экспортировать в: + Экспорт в: Экспорт проводок - Экспортировать все - Экспортировать все проводки, а не только новые. - Ошибка при экспорте %1$s + Экспорт всех проводок + По умолчанию экспортируются проводки после последнего экспорта. Отметьте для экспорта всех проводок + Ошибка экспорта %1$s Экспорт - Удаление транзакций после экспорта + Удалить проводки после экспорта Все экспортированные проводки будут удалены по завершении. Настройки @@ -69,20 +69,20 @@ Отправить… Перенести - Перенести %1$d проводк(у,и,ок) + Перенести %1$d проводку (и) Счёт-получатель Доступ к карте памяти - Невозможно перенести проводки.\nСчёт-получатель использует другую валюту. + Невозможно перенести проводки.\nСчёт-получатель в другой валюте Общие О программе Выберите валюту по умолчанию Валюта по умолчанию - Валюта для новых счетов + Валюта новых счетов Запись проводок в Gnucash для Android Создание счетов в Gnucash для Android Ваши данные Gnucash Чтение и модификация данных Gnucash - Запись транзакций в GnuCash + Запись проводок в GnuCash Создайте учетные записи в GnuCash Показать счёт Создать счета @@ -118,14 +118,14 @@ Вы действительно хотите удалить эту проводку? Экспорт Экспорт всех проводок - Всегда удалять экспортированное + Всегда удалять после экспорта E-mail для экспорта Почтовый ящик по умолчанию для экспорта. Можете менять его в процессе. Трансфертный счёт Все проводки будут переводами с одного счёта на другой. Вести двойную запись Баланс - Введите имя создаваемого счёта + Введите имя нового счёта Валюта Родительский счёт Использовать XML-заголовок OFX @@ -139,15 +139,15 @@ - Улучшена обработка запланированных операций\n - Множество других исправлений ошибок и улучшений\n - Отказаться + Отмена Введите сумму, чтобы сохранить проводку - Не могу редактировать мультивалютные проводки - Импортировать счета из GnuCash - Импортировать счета - Произошла ошибка при импорте счетов из GnuCash + Мультивалютные проводки не изменяются + Импорт счетов из GnuCash + Импорт счетов + Ошибка импорта счетов из GnuCash Счета из GnuCash успешно импортированы Импорт структуры счетов из GnuCash для ПК - Импортировать счета из GnuCash + Импорт счетов из GnuCash Удалить все счета из базы. Все проводки тоже удалятся. Удалить все счета Счета @@ -165,14 +165,14 @@ Формат экспорта по умолчанию Формат файла, используемый по умолчанию при экспорте Экспортировать проводки… - Запланированная проводка + Периодическая проводка Дисбаланс Проводки экспортируются - Нет запланированных проводок. - Запланированная проводка успешно удалена + Нет периодических проводок. + Периодическая проводка удалена Виртуальный счёт - Трансфертный счёт по умолчанию + Счет-получатель по умолчанию %d дочерний счёт %d шт. дочерних счетов @@ -208,30 +208,29 @@ Последние Избранные Все - Создание структуры счетов GnuCash по умолчанию - Создание счетов по умолчанию - Новая книга будет открыта с помощью учетной записи по умолчанию\n\nВаши текущие счета и операции не будут изменяться! + Создать структуру счетов GnuCash по умолчанию + Создать счета по умолчанию + Новая книга будет открыта с помощью учетной записи по умолчанию\n\nВаши текущие счета и операции не изменятся! Запланированные проводки Добро - пожаловать в GnuCash для Android!\nВы можете как создать структуру - часто используемых счетов, так и импортировать её из GnuCash.\n\nОбе + пожаловать в GnuCash для Android!\nВы можете создать структуру счетов или импортировать её из GnuCash.\n\nОбе возможности будут доступны из настроек приложения, если вы захотите сделать это позже. Запланированные проводки Выберите получателя экспорта - Памятка + Заметка Расход Приход Возврат Депозит Платёж - Долг + Оплата Уменьшение Увеличение - Входящее + Приход Скидка - Трата + Расход Счёт Чек Покупка @@ -240,9 +239,9 @@ Нет резервной копии Начальное сальдо Собственные средства - Включите чтобы сохранить текущий баланс (перед удалением проводок) как новое начальное сальдо после их удаления + Сохранить текущий баланс (перед удалением проводок) как новое начальное сальдо после их удаления - Сохранять начальное сальдо счетов + Сохранить начальное сальдо счетов OFX не поддерживает двойную запись Создаёт на каждую валюту отдельный QIF-файл Части проводки @@ -277,7 +276,7 @@ Есть вложенные счета.\nЧто с ними делать? Удалить проводки Создайте и укажите трансфертный счёт или отключите двойную запись в настройках - Тыкните, чтобы создать расписание + Создать расписание Восстановить из резервной копии… Резервное копирование & экспорт Включить DropBox @@ -409,9 +408,9 @@ Бюджеты Денежный поток Бюджеты - Включить компактный вид - Разрешить всегда использовать компактный вид для списка транзакций - Недопустимый валютный курс + Компактный вид + Использовать компактный вид для списка проводок + Недопустимый курс валют например, 1 %1$s = x.xx %2$s Неверная сумма diff --git a/app/src/test/java/org/gnucash/android/test/unit/model/ScheduledActionTest.java b/app/src/test/java/org/gnucash/android/test/unit/model/ScheduledActionTest.java index c1a0b9275..89b0fe430 100644 --- a/app/src/test/java/org/gnucash/android/test/unit/model/ScheduledActionTest.java +++ b/app/src/test/java/org/gnucash/android/test/unit/model/ScheduledActionTest.java @@ -105,11 +105,11 @@ public void testComputingNextScheduledExecution(){ recurrence.setPeriodStart(new Timestamp(startDate.getMillis())); scheduledAction.setRecurrence(recurrence); - assertThat(scheduledAction.computeNextScheduledExecutionTime()).isEqualTo(startDate.getMillis()); + assertThat(scheduledAction.computeNextCountBasedScheduledExecutionTime()).isEqualTo(startDate.getMillis()); scheduledAction.setExecutionCount(3); DateTime expectedTime = new DateTime(2016, 2, 15, 12, 0); - assertThat(scheduledAction.computeNextScheduledExecutionTime()).isEqualTo(expectedTime.getMillis()); + assertThat(scheduledAction.computeNextCountBasedScheduledExecutionTime()).isEqualTo(expectedTime.getMillis()); } @Test 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 3827398d6..4b8dfda77 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 @@ -280,12 +280,28 @@ public void recurringTransactions_shouldHaveScheduledActionUID(){ assertThat(transactionsDbAdapter.getRecordsCount()).isZero(); } + /** + * Scheduled backups should run only once. + * + *

Backups may have been missed since the last run, but still only + * one should be done.

+ * + *

For example, if we have set up a daily backup, the last one + * 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(){ ScheduledAction scheduledBackup = new ScheduledAction(ScheduledAction.ActionType.BACKUP); - scheduledBackup.setStartTime(new DateTime(2016, 2, 17, 17, 0).getMillis()); + scheduledBackup.setStartTime(LocalDateTime.now() + .minusMonths(4).minusDays(2).toDate().getTime()); scheduledBackup.setRecurrence(PeriodType.MONTH, 1); scheduledBackup.setExecutionCount(2); + scheduledBackup.setLastRun(LocalDateTime.now().minusMonths(2).toDate().getTime()); ExportParams backupParams = new ExportParams(ExportFormat.XML); backupParams.setExportTarget(ExportParams.ExportTarget.SD_CARD); @@ -297,12 +313,20 @@ public void scheduledBackups_shouldRunOnlyOnce(){ List actions = new ArrayList<>(); actions.add(scheduledBackup); - ScheduledActionService.processScheduledActions(actions, mDb); + // Check there's not a backup for each missed run + ScheduledActionService.processScheduledActions(actions, mDb); assertThat(scheduledBackup.getExecutionCount()).isEqualTo(3); File[] backupFiles = backupFolder.listFiles(); assertThat(backupFiles).hasSize(1); assertThat(backupFiles[0]).exists().hasExtension("gnca"); + + // Check also across service runs + ScheduledActionService.processScheduledActions(actions, mDb); + assertThat(scheduledBackup.getExecutionCount()).isEqualTo(3); + backupFiles = backupFolder.listFiles(); + assertThat(backupFiles).hasSize(1); + assertThat(backupFiles[0]).exists().hasExtension("gnca"); } /** @@ -315,6 +339,7 @@ public void scheduledBackups_shouldRunOnlyOnce(){ public void scheduledBackups_shouldNotRunBeforeNextScheduledExecution(){ ScheduledAction scheduledBackup = new ScheduledAction(ScheduledAction.ActionType.BACKUP); scheduledBackup.setStartTime(LocalDateTime.now().minusDays(2).toDate().getTime()); + scheduledBackup.setLastRun(scheduledBackup.getStartTime()); scheduledBackup.setExecutionCount(1); scheduledBackup.setRecurrence(PeriodType.WEEK, 1);