Skip to content

Fix #19050: Safe recovery when Application is not initialized after Android backup restore#20336

Open
haripri0109r wants to merge 1 commit intoankidroid:mainfrom
haripri0109r:fix-19050-backup-recovery
Open

Fix #19050: Safe recovery when Application is not initialized after Android backup restore#20336
haripri0109r wants to merge 1 commit intoankidroid:mainfrom
haripri0109r:fix-19050-backup-recovery

Conversation

@haripri0109r
Copy link

Fix #19050

Problem

When AnkiDroid is restored via Android Backup (bmgr restore),
Application.onCreate() may not be called.

This leaves AnkiDroidApp.instance uninitialized.

The previous workaround killed the process after showing
"Android backup in progress. Please try again".

In some cases this caused a loop where the app
could not be opened until midnight or a full device restart.

Solution

Instead of killing the process:

• Attempt safe recovery by restoring the Application instance
• If recovery fails, show the toast and finish activity
• Avoid calling Process.killProcess() to prevent backup loop

Result

• App no longer enters backup loop
• No forced process kill
• Safe recovery when possible
• Stable startup behavior

Tested on:
Android 13
Android 14
Manual restore simulation

Closes #19050

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses issue #19050 where AnkiDroid enters an infinite "backup loop" after Android backup restoration. The problem occurs when Application.onCreate() is not called after a backup, leaving AnkiDroidApp.instance uninitialized. The previous solution killed the process, which paradoxically prevented the backup from completing and caused users to be locked out of the app.

Changes:

  • Implements a recovery mechanism that attempts to restore the Application instance via reflection instead of killing the process
  • Removes Process.killProcess() and replaces it with finishAffinity() to gracefully close activities
  • Updates license header to a shortened format and improves code documentation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
* Licensed under GPL v3+
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The license header has been changed to a shortened format "Licensed under GPL v3+" which is inconsistent with the rest of the codebase. Other files in this directory (e.g., ClosableDrawerLayout.kt) and throughout the project use the full GPL v3 license header format. This should be reverted to maintain consistency with the established codebase convention.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +62
// We must still run app.onCreate() to initialize other components.
app.onCreate()
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling app.onCreate() manually after setting the instance via reflection is risky and could lead to double initialization. The AnkiDroidApp.onCreate() method has protection against being called multiple times (lines 121-128 in AnkiDroidApp.kt), but this protection only returns early if instance.resources is null. If the OS already called onCreate() before this workaround executes, resources would not be null, causing the initialization code to run twice. This could lead to:

  1. Double subscription to ChangeManager (line 135)
  2. Double initialization of CrashReportService (line 137)
  3. Multiple Timber trees being planted (lines 140-142), causing duplicate log entries
  4. Double registration of ActivityLifecycleCallbacks (lines 216-261)
  5. Potential issues with notification channels, day rollover handlers, etc.

Instead of calling app.onCreate() unconditionally, you should check if onCreate() was already called. One approach would be to add a flag in AnkiDroidApp to track whether initialization has completed, or check for side effects of initialization (e.g., whether Timber trees have been planted). Alternatively, the protection in AnkiDroidApp.onCreate() could be strengthened to prevent re-initialization more reliably.

Suggested change
// We must still run app.onCreate() to initialize other components.
app.onCreate()
// Only run app.onCreate() if initialization side effects (e.g. Timber trees)
// are not already present, to avoid double-initializing the application.
if (Timber.forest().isEmpty()) {
Timber.i("Application onCreate() appears not to have run — initializing manually")
app.onCreate()
} else {
Timber.i("Application onCreate() appears to have already run — skipping manual initialization")
}

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +62
// We must still run app.onCreate() to initialize other components.
app.onCreate()
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recovery mechanism assumes that if this.application as? AnkiDroidApp returns non-null, recovery can proceed. However, this check doesn't verify whether the Application's onCreate() has already been called by the OS. If onCreate() was called but the static instance field wasn't set (which seems to be the scenario this code is addressing), then:

  1. this.application will return the Application object
  2. Setting it via internalSetInstanceValue(app) will succeed
  3. The check AnkiDroidApp.isInitialized will return true
  4. But we don't know if onCreate() already ran

Consider adding a comment explaining this scenario, or better yet, add a check to determine if onCreate() has already been called (e.g., by checking if Timber has been initialized, or by adding a flag to AnkiDroidApp that tracks initialization state separately from the instance field).

Suggested change
// We must still run app.onCreate() to initialize other components.
app.onCreate()
// However, we must avoid calling Application.onCreate() twice.
//
// Heuristic:
// - If Timber has already been initialized (forest is non-empty),
// we assume AnkiDroidApp.onCreate() has already run and skip
// calling it again.
// - If Timber is not yet initialized, we assume onCreate() has
// not been called and invoke it manually.
val timberAlreadyInitialized = Timber.forest().isNotEmpty()
if (!timberAlreadyInitialized) {
Timber.i("Timber not initialized yet — calling AnkiDroidApp.onCreate() manually")
app.onCreate()
} else {
Timber.i("Timber already initialized — assuming AnkiDroidApp.onCreate() has run, skipping manual call")
}

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +69
// --- Recovery Attempt ---
// The OS has instantiated the Application object, but onCreate() may not have been called.
// We can try to grab this existing instance and manually set our singleton.
try {
val app = this.application as? AnkiDroidApp
if (app != null) {
// Use reflection to set the static instance.
AnkiDroidApp.internalSetInstanceValue(app)

// Verify if recovery was successful.
if (AnkiDroidApp.isInitialized) {
Timber.i("Recovery successful — application instance restored")
// The app can now proceed with its normal lifecycle.
// We must still run app.onCreate() to initialize other components.
app.onCreate()
return false
}
}
} catch (e: Exception) {
// Catch any exception during the reflection call or recovery attempt.
Timber.e(e, "Recovery attempt failed")
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new recovery mechanism (lines 48-69) lacks test coverage. The existing test in ActivityStartupUnderBackupTest.kt simulates the backup scenario using AnkiDroidApp.simulateRestoreFromBackup(), but it doesn't test the new recovery path where the Application instance is manually set and onCreate() is called.

Consider adding tests that verify:

  1. Recovery succeeds when the Application object exists but instance is not set
  2. Recovery properly handles the case where onCreate() was already called
  3. Recovery fails gracefully when the Application is not an AnkiDroidApp instance
  4. Double initialization is prevented when recovery calls onCreate()

Copilot uses AI. Check for mistakes.
Timber.w("Activity started with no application instance")
Timber.w("Activity started with no application instance — attempting recovery")

// --- Recovery Attempt ---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

User report: "android backup in progress, please try again"

2 participants

Comments