From 9253ffe07e9f85618e34cfb73814dfb25bc18362 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 2 Sep 2026 15:43:48 -0400 Subject: [PATCH 1/5] CMM-2174: Move background-start work off the main thread Play Console names the main-thread frames behind the Sentry "Background ANR" bucket: WordPressDB open, WorkManager init, the Gravatar Quick Editor startup initializer (AndroidKeyStore/Tink in a ContentProvider), the BOOT_COMPLETED receiver, and analytics metadata DB reads. - Remove the Gravatar androidx.startup initializer from the manifest and initialize it on demand from MeFragment - Open WordPressDB lazily (synchronized getter) and warm it on IO - Initialize WorkManager on demand via Configuration.Provider; enqueue the periodic upload work off the main thread - Run NotificationsPendingDraftsReceiver work on a background thread with goAsync() - Move refreshMetadata, install/upgrade tracks and the notification state check into one ordered IO coroutine; move the media-sanitize site lookup into its worker thread - Drop the dead fatal_db_error launch check and its string --- WordPress/build.gradle | 1 + .../wordpress/android/InitializationRule.kt | 4 + .../java/org/wordpress/android/TestUtils.java | 44 ------- WordPress/src/main/AndroidManifest.xml | 7 ++ .../org/wordpress/android/AppInitializer.kt | 115 +++++++++--------- .../java/org/wordpress/android/WordPress.kt | 59 ++++++++- .../android/datasets/NotificationsTable.java | 2 +- .../android/datasets/PeopleTable.java | 4 +- .../android/datasets/PublicizeTable.java | 4 +- .../android/datasets/SiteSettingsTable.java | 8 +- .../android/datasets/UserSuggestionTable.java | 4 +- .../android/modules/AppComponent.java | 3 + .../ui/AddQuickPressShortcutActivity.java | 2 +- .../wordpress/android/ui/WPLaunchActivity.kt | 7 -- .../wordpress/android/ui/main/MeFragment.kt | 17 +++ .../NotificationsPendingDraftsReceiver.java | 16 +++ WordPress/src/main/res/values-ar/strings.xml | 1 - WordPress/src/main/res/values-az/strings.xml | 1 - WordPress/src/main/res/values-bg/strings.xml | 1 - WordPress/src/main/res/values-cs/strings.xml | 1 - WordPress/src/main/res/values-cy/strings.xml | 1 - WordPress/src/main/res/values-de/strings.xml | 1 - WordPress/src/main/res/values-el/strings.xml | 1 - .../src/main/res/values-en-rAU/strings.xml | 1 - .../src/main/res/values-en-rCA/strings.xml | 1 - .../src/main/res/values-en-rGB/strings.xml | 1 - .../src/main/res/values-es-rCL/strings.xml | 1 - .../src/main/res/values-es-rCO/strings.xml | 1 - .../src/main/res/values-es-rMX/strings.xml | 1 - .../src/main/res/values-es-rVE/strings.xml | 1 - WordPress/src/main/res/values-es/strings.xml | 1 - WordPress/src/main/res/values-eu/strings.xml | 1 - .../src/main/res/values-fr-rCA/strings.xml | 1 - WordPress/src/main/res/values-fr/strings.xml | 1 - WordPress/src/main/res/values-gd/strings.xml | 1 - WordPress/src/main/res/values-gl/strings.xml | 1 - WordPress/src/main/res/values-he/strings.xml | 1 - WordPress/src/main/res/values-hi/strings.xml | 1 - WordPress/src/main/res/values-hr/strings.xml | 1 - WordPress/src/main/res/values-id/strings.xml | 1 - WordPress/src/main/res/values-is/strings.xml | 1 - WordPress/src/main/res/values-it/strings.xml | 1 - WordPress/src/main/res/values-ja/strings.xml | 1 - WordPress/src/main/res/values-kmr/strings.xml | 1 - WordPress/src/main/res/values-ko/strings.xml | 1 - WordPress/src/main/res/values-lv/strings.xml | 1 - WordPress/src/main/res/values-mk/strings.xml | 1 - WordPress/src/main/res/values-ms/strings.xml | 1 - WordPress/src/main/res/values-nb/strings.xml | 1 - WordPress/src/main/res/values-nl/strings.xml | 1 - WordPress/src/main/res/values-pl/strings.xml | 1 - .../src/main/res/values-pt-rBR/strings.xml | 1 - WordPress/src/main/res/values-ro/strings.xml | 1 - WordPress/src/main/res/values-ru/strings.xml | 1 - WordPress/src/main/res/values-sk/strings.xml | 1 - WordPress/src/main/res/values-sq/strings.xml | 1 - WordPress/src/main/res/values-sr/strings.xml | 1 - WordPress/src/main/res/values-sv/strings.xml | 1 - WordPress/src/main/res/values-th/strings.xml | 1 - WordPress/src/main/res/values-tr/strings.xml | 1 - WordPress/src/main/res/values-vi/strings.xml | 1 - .../src/main/res/values-zh-rCN/strings.xml | 1 - .../src/main/res/values-zh-rHK/strings.xml | 1 - .../src/main/res/values-zh-rTW/strings.xml | 1 - WordPress/src/main/res/values/strings.xml | 1 - gradle/libs.versions.toml | 2 + 66 files changed, 174 insertions(+), 174 deletions(-) diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 802f8966cc96..dbd793a32013 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -422,6 +422,7 @@ dependencies { implementation(libs.androidx.lifecycle.livedata.core) implementation(libs.androidx.lifecycle.livedata.main) implementation(libs.androidx.lifecycle.process) + implementation(libs.androidx.startup.runtime) implementation(libs.android.volley) implementation(libs.google.play.review) implementation(libs.google.mlkit.barcode.scanning.common) diff --git a/WordPress/src/androidTest/java/org/wordpress/android/InitializationRule.kt b/WordPress/src/androidTest/java/org/wordpress/android/InitializationRule.kt index 8759b9bc9e66..fa76a763cd16 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/InitializationRule.kt +++ b/WordPress/src/androidTest/java/org/wordpress/android/InitializationRule.kt @@ -5,6 +5,7 @@ import dagger.hilt.EntryPoint import dagger.hilt.EntryPoints import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.runBlocking import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement @@ -27,6 +28,9 @@ class InitializationRule : TestRule { AppInitializerEntryPoint::class.java ).appInitializer() instrumentation.runOnMainSync { appInitializer.init() } + // init() enqueues the periodic upload work asynchronously; wait for it so the enqueue lands in + // the real WorkManager before a test swaps in WorkManagerTestInitHelper's instance. + runBlocking { appInitializer.periodicUploadEnqueueJob?.join() } application.initializer = appInitializer diff --git a/WordPress/src/androidTest/java/org/wordpress/android/TestUtils.java b/WordPress/src/androidTest/java/org/wordpress/android/TestUtils.java index b0491104cb85..55fd855344d0 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/TestUtils.java +++ b/WordPress/src/androidTest/java/org/wordpress/android/TestUtils.java @@ -3,18 +3,12 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; -import android.database.sqlite.SQLiteDatabase; -import android.text.TextUtils; import androidx.preference.PreferenceManager; import org.greenrobot.eventbus.EventBus; import org.wordpress.android.util.DateTimeUtils; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.lang.reflect.Field; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -24,49 +18,11 @@ import java.util.Locale; import java.util.Map; -import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; public class TestUtils { private static final String DATABASE_NAME = "wordpress"; - public static SQLiteDatabase loadDBFromDump(Context targetContext, Context testContext, String filename) { - targetContext.deleteDatabase(DATABASE_NAME); - WordPress.wpDB = new WordPressDB(targetContext); - - Field dbField; - try { - dbField = WordPressDB.class.getDeclaredField("db"); - dbField.setAccessible(true); - SQLiteDatabase db = (SQLiteDatabase) dbField.get(WordPress.wpDB); - assertNotNull(db); - - // Load file - InputStream is = testContext.getAssets().open(filename); - InputStreamReader inputStreamReader = new InputStreamReader(is); - BufferedReader f = new BufferedReader(inputStreamReader); - for (String line = f.readLine(); line != null; line = f.readLine()) { - if (TextUtils.isEmpty(line)) { - continue; - } - try { - db.execSQL(line); - } catch (android.database.sqlite.SQLiteException e) { - // ignore import errors - } - } - f.close(); - return db; - } catch (NoSuchFieldException e) { - assertTrue(e.toString(), false); - } catch (IllegalAccessException e) { - assertTrue(e.toString(), false); - } catch (IOException e) { - assertTrue(e.toString(), false); - } - return null; - } - public static void resetEventBus() { Field dbField; try { diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 2ce7833dc6dc..375e717a27b6 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -1389,6 +1389,13 @@ android:name="androidx.work.WorkManagerInitializer" android:value="androidx.startup" tools:node="remove" /> + + = Build.VERSION_CODES.VANILLA_ICE_CREAM) { @@ -63,19 +81,50 @@ abstract class WordPress : Application(), coil.ImageLoaderFactory { const val REMOTE_SITE_ID = "REMOTE_SITE_ID" const val USER_AGENT_APPNAME = "wp-android" + // Use service ids near the int max to avoid collisions with existing JobService ids + // The minimum range size is 1000, but we can easily give 10000. + private const val WORK_MANAGER_ID_RANGE_MAX = Int.MAX_VALUE + private const val WORK_MANAGER_ID_RANGE_MIN = WORK_MANAGER_ID_RANGE_MAX - 10000 + lateinit var versionName: String - lateinit var wpDB: WordPressDB var appIsInTheBackground = true + @Volatile + private var wpDBInstance: WordPressDB? = null + + /** + * The legacy app database. Opened lazily (and warmed up off the main thread by [AppInitializer]) so + * that background process starts don't run the SQLite open + migrations on the main thread. + */ + @JvmStatic + val wpDB: WordPressDB + get() = wpDBInstance ?: synchronized(this) { + wpDBInstance ?: openWpDb(getContext()).also { wpDBInstance = it } + } + + private fun openWpDb(context: Context): WordPressDB { + return try { + WordPressDB(context) + } catch (e: SQLiteException) { + recreateWpDb(context, e) + } catch (e: SQLException) { + recreateWpDb(context, e) + } + } + + private fun recreateWpDb(context: Context, e: Exception): WordPressDB { + AppLog.e(T.DB, e) + AppLog.e(T.DB, "Invalid database, sign out user and delete database") + WordPressDB.deleteDatabase(context) + return WordPressDB(context) + } + @JvmField var requestQueue: RequestQueue? = null @JvmField var imageLoader: FluxCImageLoader? = null - val isWpDBInitialized - get() = ::wpDB.isInitialized - @JvmStatic fun getBitmapCache() = AppInitializer.getBitmapCache() diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java index 890ca77e99c6..2c21509b1397 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java +++ b/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java @@ -25,7 +25,7 @@ public class NotificationsTable { private static final String NOTIFICATIONS_TABLE = "tbl_notifications"; private static SQLiteDatabase getDb() { - return WordPress.wpDB.getDatabase(); + return WordPress.getWpDB().getDatabase(); } public static final int NOTES_TO_RETRIEVE = 200; diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/PeopleTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/PeopleTable.java index b02fcdf85875..0aecc6547602 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/PeopleTable.java +++ b/WordPress/src/main/java/org/wordpress/android/datasets/PeopleTable.java @@ -23,11 +23,11 @@ public class PeopleTable { private static final String VIEWERS_TABLE = "people_viewers"; private static SQLiteDatabase getReadableDb() { - return WordPress.wpDB.getDatabase(); + return WordPress.getWpDB().getDatabase(); } private static SQLiteDatabase getWritableDb() { - return WordPress.wpDB.getDatabase(); + return WordPress.getWpDB().getDatabase(); } public static void createTables(SQLiteDatabase db) { diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/PublicizeTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/PublicizeTable.java index 52968d43263e..8bbc263ede18 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/PublicizeTable.java +++ b/WordPress/src/main/java/org/wordpress/android/datasets/PublicizeTable.java @@ -51,11 +51,11 @@ public static void createTables(SQLiteDatabase db) { } private static SQLiteDatabase getReadableDb() { - return WordPress.wpDB.getDatabase(); + return WordPress.getWpDB().getDatabase(); } private static SQLiteDatabase getWritableDb() { - return WordPress.wpDB.getDatabase(); + return WordPress.getWpDB().getDatabase(); } public static void resetServicesTable(SQLiteDatabase db) { diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java index 0ac3dca08af1..a152370a9df6 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java +++ b/WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java @@ -48,7 +48,7 @@ public static void addSharingColumnsToSiteSettingsTable(SQLiteDatabase db) { public static SparseArrayCompat getAllCategories() { String sqlCommand = sqlSelectAllCategories() + ";"; - Cursor cursor = WordPress.wpDB.getDatabase().rawQuery(sqlCommand, null); + Cursor cursor = WordPress.getWpDB().getDatabase().rawQuery(sqlCommand, null); try { if (cursor == null || !cursor.moveToFirst() || cursor.getCount() == 0) { return null; @@ -77,7 +77,7 @@ public static Cursor getSettings(long id) { String whereClause = sqlWhere(SiteSettingsModel.ID_COLUMN_NAME, Long.toString(id)); String sqlCommand = sqlSelectAllSettings() + whereClause + ";"; - return WordPress.wpDB.getDatabase().rawQuery(sqlCommand, null); + return WordPress.getWpDB().getDatabase().rawQuery(sqlCommand, null); } public static void saveCategory(CategoryModel category) { @@ -86,7 +86,7 @@ public static void saveCategory(CategoryModel category) { } ContentValues values = category.serializeToDatabase(); - category.isInLocalTable = WordPress.wpDB.getDatabase().insertWithOnConflict( + category.isInLocalTable = WordPress.getWpDB().getDatabase().insertWithOnConflict( CATEGORIES_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE) != -1; } @@ -106,7 +106,7 @@ public static void saveSettings(SiteSettingsModel settings) { } ContentValues values = settings.serializeToDatabase(); - settings.isInLocalTable = WordPress.wpDB.getDatabase().insertWithOnConflict( + settings.isInLocalTable = WordPress.getWpDB().getDatabase().insertWithOnConflict( SiteSettingsModel.SETTINGS_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE) != -1; saveCategories(settings.categories); diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/UserSuggestionTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/UserSuggestionTable.java index ae41b10f6533..80bfb6745ce3 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/UserSuggestionTable.java +++ b/WordPress/src/main/java/org/wordpress/android/datasets/UserSuggestionTable.java @@ -45,11 +45,11 @@ public static void reset(SQLiteDatabase db) { } private static SQLiteDatabase getReadableDb() { - return WordPress.wpDB.getDatabase(); + return WordPress.getWpDB().getDatabase(); } private static SQLiteDatabase getWritableDb() { - return WordPress.wpDB.getDatabase(); + return WordPress.getWpDB().getDatabase(); } public static void insertSuggestionsForSite(final long siteId, final List suggestions) { diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java index 7c9d265446b3..ddd3cd3445ed 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -175,6 +175,7 @@ import org.wordpress.android.ui.whatsnew.FeatureAnnouncementListAdapter; import org.wordpress.android.util.WPWebViewClient; import org.wordpress.android.util.image.getters.WPCustomImageGetter; +import org.wordpress.android.workers.WordPressWorkersFactory; import dagger.hilt.EntryPoint; import dagger.hilt.InstallIn; @@ -183,6 +184,8 @@ @InstallIn(SingletonComponent.class) @EntryPoint public interface AppComponent { + WordPressWorkersFactory wordPressWorkersFactory(); + void inject(WPMainActivity object); void inject(MediaUploadHandler object); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java index 53670b489e17..0535bca372c8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java @@ -152,7 +152,7 @@ public void onClick(DialogInterface dialog, int which) { String shortcutName = quickPressShortcutName.getText().toString(); - WordPress.wpDB.addQuickPressShortcut(siteIds[position], shortcutName); + WordPress.getWpDB().addQuickPressShortcut(siteIds[position], shortcutName); ShortcutInfoCompat pinShortcutInfo = new ShortcutInfoCompat.Builder(getApplicationContext(), shortcutName) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/WPLaunchActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/WPLaunchActivity.kt index 7d9818663e8a..3e07232dd7ef 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/WPLaunchActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/WPLaunchActivity.kt @@ -6,11 +6,9 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.wordpress.android.R -import org.wordpress.android.WordPress import org.wordpress.android.ui.main.WPMainActivity import org.wordpress.android.util.MissingSplitsUtils.isMissingSplits import org.wordpress.android.util.ProfilingUtils -import org.wordpress.android.util.ToastUtils @SuppressLint("CustomSplashScreen") class WPLaunchActivity : AppCompatActivity() { @@ -50,11 +48,6 @@ class WPLaunchActivity : AppCompatActivity() { } private fun launchWPMainActivity() { - if (!WordPress.isWpDBInitialized) { - ToastUtils.showToast(this, R.string.fatal_db_error, ToastUtils.Duration.LONG) - finish() - return - } val intent = Intent(this, WPMainActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) intent.action = getIntent().action diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt index 57d422ddb276..78b4929bec75 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt @@ -16,6 +16,8 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import androidx.startup.AppInitializer +import androidx.startup.Initializer import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.gravatar.quickeditor.GravatarQuickEditor @@ -257,6 +259,7 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { ) { try { appLogWrapper.d(AppLog.T.MAIN, "Gravatar: Showing Gravatar quick editor: ${page.value}") + initGravatarQuickEditor() GravatarQuickEditor.show( fragment = this@MeFragment, gravatarQuickEditorParams = GravatarQuickEditorParams { @@ -294,6 +297,18 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { } } + /** + * The Gravatar library's androidx.startup initializer is removed from the manifest because it does + * AndroidKeyStore work on the main thread at process start (a source of background ANRs). Initialize it on + * demand instead; [AppInitializer.initializeComponent] is idempotent, so repeated opens are safe. The + * initializer class is internal to the library, hence the lookup by name (R8 keeps every Initializer). + */ + @Suppress("UNCHECKED_CAST") + private fun initGravatarQuickEditor() { + val initializer = Class.forName(GRAVATAR_QUICK_EDITOR_INITIALIZER) as Class> + AppInitializer.getInstance(requireContext()).initializeComponent(initializer) + } + private fun withVeryfiedEmail( @StringRes unverifiedEmailMessageRes: Int, action: () -> Unit, @@ -631,6 +646,8 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { companion object { private const val IS_DISCONNECTING = "IS_DISCONNECTING" private const val GRAVATAR_URL = "https://www.gravatar.com" + private const val GRAVATAR_QUICK_EDITOR_INITIALIZER = + "com.gravatar.quickeditor.initializer.QuickEditorContainerInitializer" fun newInstance(): MeFragment { return MeFragment() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java index 098dff1cb09c..5150373bd2aa 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java @@ -53,6 +53,22 @@ public class NotificationsPendingDraftsReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { ((WordPress) context.getApplicationContext()).component().inject(this); + // Both branches read posts from the database. Doing that on the main thread inside a broadcast + // (in particular BOOT_COMPLETED, which starts the process in the background) shows up as ANRs, so + // the work is moved to a background thread and the broadcast is kept alive with goAsync(). + final PendingResult pendingResult = goAsync(); + new Thread(() -> { + try { + handleIntent(context, intent); + } catch (Throwable t) { + AppLog.e(AppLog.T.NOTIFS, "Pending Drafts Receiver failed", t); + } finally { + pendingResult.finish(); + } + }, "PendingDraftsReceiver").start(); + } + + private void handleIntent(Context context, Intent intent) { // for the case of being spanned after device restarts, get the latest drafts // and check the lastUpdated String action = intent.getAction(); diff --git a/WordPress/src/main/res/values-ar/strings.xml b/WordPress/src/main/res/values-ar/strings.xml index 0d2fd2c66b05..1bc382bf1a20 100644 --- a/WordPress/src/main/res/values-ar/strings.xml +++ b/WordPress/src/main/res/values-ar/strings.xml @@ -3833,7 +3833,6 @@ Language: ar وسائط جديدة التغيرات المحلية إعدادات المقالة - حدث خطأ اثناء إنشاء قاعدة بيانات التطبيق. حاول إعادة تنصيب التطبيق. المحاذاة الأفقية سلة المهملات مزعج diff --git a/WordPress/src/main/res/values-az/strings.xml b/WordPress/src/main/res/values-az/strings.xml index ff81a2dc3302..c5921b92d53d 100644 --- a/WordPress/src/main/res/values-az/strings.xml +++ b/WordPress/src/main/res/values-az/strings.xml @@ -125,7 +125,6 @@ Language: az Proqram jurnalı Yerli dəyişikliklər WordPress bloqu - Proqram üçün verilənlər bazası yaradılarkən bir xəta baş verdi. Proqramı təkrar qurmağa cəhd edin. Qeydiyyat gərəklidir Brauzerdə bax Yeni kateqoriya əlavə et diff --git a/WordPress/src/main/res/values-bg/strings.xml b/WordPress/src/main/res/values-bg/strings.xml index 90de587b9728..5b33b8b0f758 100644 --- a/WordPress/src/main/res/values-bg/strings.xml +++ b/WordPress/src/main/res/values-bg/strings.xml @@ -817,7 +817,6 @@ Language: bg Локална чернова Нов файл Текст на връзката (по избор) - Възникна грешка при създаване на базата от данни на приложението. Моля преинсталирайте приложението. Предстоящ Възникна грешка при зареждане на тази плубликация. Опреснете Вашите публикации и опитайте отново. Кошче diff --git a/WordPress/src/main/res/values-cs/strings.xml b/WordPress/src/main/res/values-cs/strings.xml index ac2008301991..6d79da6e2a8c 100644 --- a/WordPress/src/main/res/values-cs/strings.xml +++ b/WordPress/src/main/res/values-cs/strings.xml @@ -3863,7 +3863,6 @@ Language: cs_CZ Schválit Zamítnout Přesunout do koše? - Vyskytla se chyba při vytváření databáze aplikace. Zkuste přeinstalovat tuto aplikaci. Místní změny Protokol aplikace Nastavení příspěvku diff --git a/WordPress/src/main/res/values-cy/strings.xml b/WordPress/src/main/res/values-cy/strings.xml index b8a039839452..9aae351d00df 100644 --- a/WordPress/src/main/res/values-cy/strings.xml +++ b/WordPress/src/main/res/values-cy/strings.xml @@ -461,7 +461,6 @@ Language: cy_GB Cyfryngau newydd Newidiadau lleol Blog WordPress - Digwyddodd gwall wrth greu cronfa ddata\'r ap. Ceisiwch ailosod yr ap. Cofnod yr ap Gwiriwch fod URL y blog yn ddilys Rhannu dolen diff --git a/WordPress/src/main/res/values-de/strings.xml b/WordPress/src/main/res/values-de/strings.xml index 6629a7c8c6dd..75129b144698 100644 --- a/WordPress/src/main/res/values-de/strings.xml +++ b/WordPress/src/main/res/values-de/strings.xml @@ -3837,7 +3837,6 @@ Language: de Dieses Schlagwort konnte nicht entfernt werden Dieses Schlagwort konnte nicht hinzugefügt werden Anwendungsprotokoll - Bei der Erstellung der App-Datenbank ist ein Fehler aufgetreten. Versuche die App erneut zu installieren. Die Medien konnten zu diesem Zeitpunkt nicht aktualisiert werden WordPress-Blog Lokale Änderungen diff --git a/WordPress/src/main/res/values-el/strings.xml b/WordPress/src/main/res/values-el/strings.xml index 9040e767ed0a..25ed9687f0b5 100644 --- a/WordPress/src/main/res/values-el/strings.xml +++ b/WordPress/src/main/res/values-el/strings.xml @@ -1023,7 +1023,6 @@ Language: el_GR Αδυναμία δημιουργίας προσωρινού αρχείου για μεταφόρτωση πολυμέσων. Βεβαιωθείτε ότι υπάρχει αρκετός ελεύθερος χώρος στη συσκευή σας. Απαιτείται εξουσιοδότηση Νέο πολυμέσο - Προέκυψε σφάλμα κατά τη δημιουργία της βάσης δεδομένων της εφαρμογής. Δοκιμάστε να την εγκαταστήσετε ξανά. Αρχείο καταγραφής εφαρμογής Αδυναμία προσθήκης αυτής της ετικέτας Αδυναμία αφαίρεσης αυτής της ετικέτας diff --git a/WordPress/src/main/res/values-en-rAU/strings.xml b/WordPress/src/main/res/values-en-rAU/strings.xml index bce05cdf8590..1db3137d2d9e 100644 --- a/WordPress/src/main/res/values-en-rAU/strings.xml +++ b/WordPress/src/main/res/values-en-rAU/strings.xml @@ -1494,7 +1494,6 @@ Language: en_AU New media Local changes WordPress blog - An error occurred while creating the app database. Try reinstalling the app. Application log Remove site No notifications…yet. diff --git a/WordPress/src/main/res/values-en-rCA/strings.xml b/WordPress/src/main/res/values-en-rCA/strings.xml index cbe358608dca..1084daa8847c 100644 --- a/WordPress/src/main/res/values-en-rCA/strings.xml +++ b/WordPress/src/main/res/values-en-rCA/strings.xml @@ -3465,7 +3465,6 @@ Language: en_CA New media Local changes WordPress blog - An error occurred while creating the app database. Try reinstalling the app. Application log Authorization required Trashed diff --git a/WordPress/src/main/res/values-en-rGB/strings.xml b/WordPress/src/main/res/values-en-rGB/strings.xml index a961dc7ea889..960e75d465dd 100644 --- a/WordPress/src/main/res/values-en-rGB/strings.xml +++ b/WordPress/src/main/res/values-en-rGB/strings.xml @@ -3837,7 +3837,6 @@ Language: en_GB Unable to remove this tag Unable to add this tag Application log - An error occurred while creating the app database. Try reinstalling the app. Media couldn\'t be refreshed at this time WordPress blog Local changes diff --git a/WordPress/src/main/res/values-es-rCL/strings.xml b/WordPress/src/main/res/values-es-rCL/strings.xml index 54eab0f067b1..fd936c3fe2a2 100644 --- a/WordPress/src/main/res/values-es-rCL/strings.xml +++ b/WordPress/src/main/res/values-es-rCL/strings.xml @@ -3373,7 +3373,6 @@ Language: es_CL Cambios locales Blog de WordPress Registro de la aplicación - Hubo un error al crear la base de datos de la app. Por favor, intenta reinstalar la app. Algunos elementos multimedia no pudieron ser borrados. Prueba más tarde. No se puede actualizar Media en este momento Comprueba que la URL del sitio ingresada es válida diff --git a/WordPress/src/main/res/values-es-rCO/strings.xml b/WordPress/src/main/res/values-es-rCO/strings.xml index d53777fc6d39..eca28a24035d 100644 --- a/WordPress/src/main/res/values-es-rCO/strings.xml +++ b/WordPress/src/main/res/values-es-rCO/strings.xml @@ -3873,7 +3873,6 @@ Language: es_CO No ha sido posible encontrar el archivo a cargar. ¿Se ha borrado o cambiado de ubicación? Cuadrícula de miniaturas Ocurrió un error al acceder a este plugin - Ha ocurrido un error al crear la base de datos de la aplicación. Por favor, intenta reinstalar la aplicación. Algunos medios no se han podido borrar en este momento. Inténtalo de nuevo más tarde. Ajustes de entrada No se ha podido eliminar esta etiqueta diff --git a/WordPress/src/main/res/values-es-rMX/strings.xml b/WordPress/src/main/res/values-es-rMX/strings.xml index ac0b1ffed2fc..08f07581a6ae 100644 --- a/WordPress/src/main/res/values-es-rMX/strings.xml +++ b/WordPress/src/main/res/values-es-rMX/strings.xml @@ -1950,7 +1950,6 @@ Language: es_MX Rechazar Blog de WordPress Registro de la aplicación - Hubo un error al crear la base de datos de la app. Por favor, intenta reinstalar la app. No se pueden actualizar los medios en este momento Nuevo elemento multimedia Nueva entrada diff --git a/WordPress/src/main/res/values-es-rVE/strings.xml b/WordPress/src/main/res/values-es-rVE/strings.xml index abe6619789d7..badde3989829 100644 --- a/WordPress/src/main/res/values-es-rVE/strings.xml +++ b/WordPress/src/main/res/values-es-rVE/strings.xml @@ -2657,7 +2657,6 @@ Language: es_VE No ha sido posible encontrar el archivo a cargar. ¿Se ha borrado o cambiado de ubicación? Ocurrió un error al acceder a este plugin Cuadrícula de miniaturas - Ha ocurrido un error al crear la base de datos de la aplicación. Por favor, intenta reinstalar la aplicación. Algunos medios no se han podido borrar en este momento. Inténtalo de nuevo más tarde. Ajustes de entrada Compartir enlace diff --git a/WordPress/src/main/res/values-es/strings.xml b/WordPress/src/main/res/values-es/strings.xml index 4dd39568b0ab..05061fface93 100644 --- a/WordPress/src/main/res/values-es/strings.xml +++ b/WordPress/src/main/res/values-es/strings.xml @@ -3837,7 +3837,6 @@ Language: es No se ha podido eliminar esta etiqueta No se ha podido añadir esta etiqueta Registro de la aplicación - Ha ocurrido un error al crear la base de datos de la aplicación. Por favor, intenta reinstalar la aplicación. No se pueden actualizar los medios en este momento Blog de WordPress Cambios locales diff --git a/WordPress/src/main/res/values-eu/strings.xml b/WordPress/src/main/res/values-eu/strings.xml index 47772fbc9105..2d72a16b02eb 100644 --- a/WordPress/src/main/res/values-eu/strings.xml +++ b/WordPress/src/main/res/values-eu/strings.xml @@ -470,7 +470,6 @@ Language: eu_ES Ezin izan da media fitxategia igotzeko behin-behineko fitxategia sortu. Ziurtatu zaitez gailuak behar haina toki duela. Bidalketa berria Media berria - Errorea gertatu da aplikazioaren datu-basea sortzean. Saiatu aplikazioa berrinstalatzen. Aplikazioaren loga Mediaren bat ezin izan da orain ezabatu. Saiatu zaitez berriz beranduago. Errore bat gertatu da bidalketa kargatzean. Eguneratu bidalketak eta saiatu berriz. diff --git a/WordPress/src/main/res/values-fr-rCA/strings.xml b/WordPress/src/main/res/values-fr-rCA/strings.xml index b50d744a7a26..58b1e1590807 100644 --- a/WordPress/src/main/res/values-fr-rCA/strings.xml +++ b/WordPress/src/main/res/values-fr-rCA/strings.xml @@ -3809,7 +3809,6 @@ Language: fr Impossible de supprimer cette étiquette Impossible d’ajouter cette étiquette Journal de l\'application - Une erreur s\'est produite lors de la création de la base de données de l\'app. Essayez de réinstaller l\'app. Impossible d’actualiser le contenu multimédia pour l’instant. Blog WordPress Changements locaux diff --git a/WordPress/src/main/res/values-fr/strings.xml b/WordPress/src/main/res/values-fr/strings.xml index b50d744a7a26..58b1e1590807 100644 --- a/WordPress/src/main/res/values-fr/strings.xml +++ b/WordPress/src/main/res/values-fr/strings.xml @@ -3809,7 +3809,6 @@ Language: fr Impossible de supprimer cette étiquette Impossible d’ajouter cette étiquette Journal de l\'application - Une erreur s\'est produite lors de la création de la base de données de l\'app. Essayez de réinstaller l\'app. Impossible d’actualiser le contenu multimédia pour l’instant. Blog WordPress Changements locaux diff --git a/WordPress/src/main/res/values-gd/strings.xml b/WordPress/src/main/res/values-gd/strings.xml index 6dc89d8baeff..2ddb4f5a7c5c 100644 --- a/WordPress/src/main/res/values-gd/strings.xml +++ b/WordPress/src/main/res/values-gd/strings.xml @@ -150,7 +150,6 @@ Language: gd_GB Meadhan ùr Atharraichean ionadail Bloga WordPress - Thachair mearachd fhad ’s a bha sinn a’ cruthachadh stòr-dàta nan aplacaidean. Feuch is stàlaich an aplacaid às ùr. Loga na h-aplacaid Dèan cinnteach gu bheil an URL a chuir thu a-steach dligheach Co-roinn an ceangal diff --git a/WordPress/src/main/res/values-gl/strings.xml b/WordPress/src/main/res/values-gl/strings.xml index c3d6f4b7420b..a20a3f8c6b02 100644 --- a/WordPress/src/main/res/values-gl/strings.xml +++ b/WordPress/src/main/res/values-gl/strings.xml @@ -3526,7 +3526,6 @@ Language: gl_ES Botar ao lixo? Lixo Rexistro da aplicación - Houbo un erro mentres se creaba a base de datos da aplicación. Proba a reinstalar a aplicación. Houbo un erro mentres se cargaba o artigo. Actualiza os artigos e téntao de novo. Non foi posible atopar o ficheiro para cargar. Terá sido eliminado ou movido? Non se poden actualizar os medios neste momento diff --git a/WordPress/src/main/res/values-he/strings.xml b/WordPress/src/main/res/values-he/strings.xml index be3f50eca8aa..714a13d1e87c 100644 --- a/WordPress/src/main/res/values-he/strings.xml +++ b/WordPress/src/main/res/values-he/strings.xml @@ -3817,7 +3817,6 @@ Language: he_IL לא ניתן להסיר תגית זו לא ניתן להוסיף תגית זו יומן יישום - אירעה שגיאה בזמן יצירת מסד הנתונים של היישום. נסה להתקין מחדש את היישום. לא ניתן לרענן את פריטי המדיה כעת אתר ב-WordPress שינויים מקומיים diff --git a/WordPress/src/main/res/values-hi/strings.xml b/WordPress/src/main/res/values-hi/strings.xml index 498a1ba57d71..e92c6f0f2bf0 100644 --- a/WordPress/src/main/res/values-hi/strings.xml +++ b/WordPress/src/main/res/values-hi/strings.xml @@ -277,7 +277,6 @@ Language: hi_IN स्थानीय प्रारूप मीडिया अपलोड करने के लिए अस्थायी फ़ाइल नहीं बनाया जा सका. आपके डिवाइस पर पर्याप्त खाली स्थान है सुनिश्चित करें. अपलोड करने के लिए मीडिया फाइल नहीं मिल सका. उसे हटाया या स्थानांतरित किया गया? - एप्लिकेशन बनाते हुए त्रुटि हुई. एप्लिकेशन को फिर से इनस्टॉल करने का प्रयास करें. एप्लीकेशन लॉग ब्लॉग हटाये पोस्ट हटाये? diff --git a/WordPress/src/main/res/values-hr/strings.xml b/WordPress/src/main/res/values-hr/strings.xml index 91e9920c2e4b..891b38369cb2 100644 --- a/WordPress/src/main/res/values-hr/strings.xml +++ b/WordPress/src/main/res/values-hr/strings.xml @@ -534,7 +534,6 @@ Language: hr Nije moguće pronaći medijski zapis za prijenos. Jeli obrisan ili premješten? Nije moguće kreirati privremeni zapis za prijenos medijskog zapisa. Provjerite da ima dovoljno slobodnog mjesta na vašem uređaju. Potrebna autorizacija - Pojavila se greška prilikom kreiranja app baze podataka. Pokušajte ponovno instalirati app. Ukloni web-stranicu Obriši objavu? Obriši stranicu? diff --git a/WordPress/src/main/res/values-id/strings.xml b/WordPress/src/main/res/values-id/strings.xml index 7a3b35bceabb..9f4a266a998e 100644 --- a/WordPress/src/main/res/values-id/strings.xml +++ b/WordPress/src/main/res/values-id/strings.xml @@ -3826,7 +3826,6 @@ Language: id Tidak bisa menghapus tag ini Tidak bisa menambahkan tag ini Log aplikasi - Terjadi gagal ketika membuat basis data app. Coba instal ulang app. Media tidak bisa disegarkan saat ini Blog WordPress Perubahan lokal diff --git a/WordPress/src/main/res/values-is/strings.xml b/WordPress/src/main/res/values-is/strings.xml index 7d98df625e84..b2be46c5284b 100644 --- a/WordPress/src/main/res/values-is/strings.xml +++ b/WordPress/src/main/res/values-is/strings.xml @@ -461,7 +461,6 @@ Language: is Bæta við nýjum flokki Heiti flokks Leyfisveitingar krafist - Villa kom upp við að stofna gagnagrunn smáforrits. Reyndu að setja smáforritið aftur upp. Atburðarskrá forrits Tengingarvilla Í bið diff --git a/WordPress/src/main/res/values-it/strings.xml b/WordPress/src/main/res/values-it/strings.xml index 9bdcb90baf12..1b7f3a9c5058 100644 --- a/WordPress/src/main/res/values-it/strings.xml +++ b/WordPress/src/main/res/values-it/strings.xml @@ -3837,7 +3837,6 @@ Language: it Impossibile rimuovere questo tag Impossibile aggiungere questo tag Registro applicazione - Si è verificato un errore durante la creazione del database delle app. Prova a reinstallare l\'app. Al momento non è stato possibile aggiornare l\'elemento multimediale Blog di WordPress Modifiche locali diff --git a/WordPress/src/main/res/values-ja/strings.xml b/WordPress/src/main/res/values-ja/strings.xml index 027645435579..b976d19d3dee 100644 --- a/WordPress/src/main/res/values-ja/strings.xml +++ b/WordPress/src/main/res/values-ja/strings.xml @@ -3815,7 +3815,6 @@ Language: ja_JP このタグを削除できません このタグを追加できません アプリケーションログ - アプリのデータベースを作成する際にエラーが発生しました。アプリをもう一度インストールしてみてください。 メディアファイルを再読み込みできませんでした WordPress ブログ ローカルの変更 diff --git a/WordPress/src/main/res/values-kmr/strings.xml b/WordPress/src/main/res/values-kmr/strings.xml index 225b0ce3868a..386442b7fea1 100644 --- a/WordPress/src/main/res/values-kmr/strings.xml +++ b/WordPress/src/main/res/values-kmr/strings.xml @@ -1890,7 +1890,6 @@ Language: ku_TR Ji bo hilxistinê dosye nehat dîtin. Ew hatiye jêbirin an jî veguheztin? Reşnivîsa cîgehî Kategoriyan hilbijêre - Dema danegeha sepanê dihat afirandin çewtiyek derket. Sepanê ji nû ve saz bike. Medya niha nayên nûkirin Destûrdarkirin pêwîst e Ji bo hilxistina medyayê dosyeya demkî nayê afirandin. Jê bawer be bila di cîhaza te de têra xwe ciyê vala hebe. diff --git a/WordPress/src/main/res/values-ko/strings.xml b/WordPress/src/main/res/values-ko/strings.xml index 8b9df0a961a8..bc0a3ef38108 100644 --- a/WordPress/src/main/res/values-ko/strings.xml +++ b/WordPress/src/main/res/values-ko/strings.xml @@ -3828,7 +3828,6 @@ Language: ko_KR 이 태그를 제거할 수 없음 이 태그를 추가할 수 없음 응용 프로그램 로그 - 앱 데이터베이스를 생성하는 동안 에러가 발생했습니다. 앱을 다시 설치하시기 바랍니다. 현재 미디어를 새로 고칠 수 없습니다 WordPress 블로그 로컬 변경 diff --git a/WordPress/src/main/res/values-lv/strings.xml b/WordPress/src/main/res/values-lv/strings.xml index 1292a8d9f3fb..b3e6d4e294c9 100644 --- a/WordPress/src/main/res/values-lv/strings.xml +++ b/WordPress/src/main/res/values-lv/strings.xml @@ -3359,7 +3359,6 @@ Language: lv Vietējās izmaiņas WordPress emuārs Medijus šobrīd nevar atsvaidzināt - Veidojot lietotņu datu bāzi, radās kļūda. Mēģiniet pārinstalēt lietotni. Lietojumprogrammu žurnāls Atlasiet kategorijas Savienojuma kļūda diff --git a/WordPress/src/main/res/values-mk/strings.xml b/WordPress/src/main/res/values-mk/strings.xml index bb2527e836cb..7150b725b3ba 100644 --- a/WordPress/src/main/res/values-mk/strings.xml +++ b/WordPress/src/main/res/values-mk/strings.xml @@ -88,7 +88,6 @@ Language: mk Зачувување на подесувањата WordPress блог Избери категории - Настана грешка при креирање на базата на податоци за апликацијата. Направи преинсталирање на апликацијата. Дневник на апликацијата Измени коментар Одобри diff --git a/WordPress/src/main/res/values-ms/strings.xml b/WordPress/src/main/res/values-ms/strings.xml index 76b9997f23be..d31e15f7e767 100644 --- a/WordPress/src/main/res/values-ms/strings.xml +++ b/WordPress/src/main/res/values-ms/strings.xml @@ -717,7 +717,6 @@ Language: ms Nama kategori Perubahan setempat Blog WordPress - Ralat berlaku semasa mencipta pangkalan data aplikasi. Cuba memasangnya semula. Log aplikasi Kebenaran diperlukan Kiriman baharu diff --git a/WordPress/src/main/res/values-nb/strings.xml b/WordPress/src/main/res/values-nb/strings.xml index 58f4eb2c4cbc..d2ad527e1500 100644 --- a/WordPress/src/main/res/values-nb/strings.xml +++ b/WordPress/src/main/res/values-nb/strings.xml @@ -2076,7 +2076,6 @@ Language: nb_NO Du har ikke tillatelse til å se mediebiblioteket En feil oppstod under innlastning av innlegget. Oppfrisk innleggene dine, og prøv igjen. Media kunne ikke bli oppfrisket for øyeblikket - En feil oppstod under oppretting av app-databasen. Prøv å reinstallere appen. Forhåndsvisningsnetting Klarte ikke å lage en midlertidig fil for medieopplastning. Vennligst sørg for at det er nok ledig lagring på enheten din. Klarte ikke å finne filen som skulle lastes opp. Ble den kanskje slettet eller flyttet? diff --git a/WordPress/src/main/res/values-nl/strings.xml b/WordPress/src/main/res/values-nl/strings.xml index 25fdc3663c36..2df98b5a88cf 100644 --- a/WordPress/src/main/res/values-nl/strings.xml +++ b/WordPress/src/main/res/values-nl/strings.xml @@ -3837,7 +3837,6 @@ Language: nl Deze tag kan niet worden verwijderd Deze tag kan niet worden toegevoegd Applicatie log - Er heeft zich een fout voorgedaan bij het aanmaken van de database van de app. Probeer de app opnieuw te installeren. Media konden op dit moment niet worden vernieuwd WordPress blog Lokale wijzigingen diff --git a/WordPress/src/main/res/values-pl/strings.xml b/WordPress/src/main/res/values-pl/strings.xml index a74d6091374d..1beb13fce2bb 100644 --- a/WordPress/src/main/res/values-pl/strings.xml +++ b/WordPress/src/main/res/values-pl/strings.xml @@ -3837,7 +3837,6 @@ Language: pl Nie udało się usunąć tagu Nie udało się dodać tagu Dziennik aplikacji - Wystąpił błąd podczas tworzenia bazy danych. Spróbuj przeinstalować aplikacje. Nie można było odświeżyć mediów w tym momencie WordPress blog Zmiany lokalne diff --git a/WordPress/src/main/res/values-pt-rBR/strings.xml b/WordPress/src/main/res/values-pt-rBR/strings.xml index cdb084193cbe..c3ab24584370 100644 --- a/WordPress/src/main/res/values-pt-rBR/strings.xml +++ b/WordPress/src/main/res/values-pt-rBR/strings.xml @@ -3514,7 +3514,6 @@ Language: pt_BR Não foi possível remover a tag Não foi possível adicionar a tag Log do aplicativo - Ocorreu um erro ao criar o banco de dados do aplicativo. Tente reinstalar o aplicativo. Arquivo de mídia não pôde ser atualizado neste momento Blog WordPress Alterações locais diff --git a/WordPress/src/main/res/values-ro/strings.xml b/WordPress/src/main/res/values-ro/strings.xml index 3ef343ea62bf..f8d00fbe0fbc 100644 --- a/WordPress/src/main/res/values-ro/strings.xml +++ b/WordPress/src/main/res/values-ro/strings.xml @@ -3854,7 +3854,6 @@ Language: ro Media nouă Schimbări locale Blog WordPress - A apărut o eroare în timp ce se creea baza de date a aplicației. Încearcă reinstalarea ei. Setări pagină Ciornă locală Media nu a putut fi reîmprospătată în acest moment diff --git a/WordPress/src/main/res/values-ru/strings.xml b/WordPress/src/main/res/values-ru/strings.xml index 8fa3592ec892..afac3c6db649 100644 --- a/WordPress/src/main/res/values-ru/strings.xml +++ b/WordPress/src/main/res/values-ru/strings.xml @@ -3837,7 +3837,6 @@ Language: ru Не удается удалить эту метку Не удается подписаться на эту метку Лог приложения - Произошла ошибка при создании базы данных приложения- попробуйте его переустановить. В данный момент не удалось обновить медиафайлы. Блог WordPress Локальные изменения diff --git a/WordPress/src/main/res/values-sk/strings.xml b/WordPress/src/main/res/values-sk/strings.xml index e8dc1cf9bc1e..9e408c8b0816 100644 --- a/WordPress/src/main/res/values-sk/strings.xml +++ b/WordPress/src/main/res/values-sk/strings.xml @@ -1266,7 +1266,6 @@ Language: sk Kôš Text odkazu (voliteľný) Miestne zmeny - Počas vytvárania databázy sa vyskytla chyba. Preinštalujte aplikáciu. Nemáte práva na zobrazenie knižnice súborov Nový súbor Počas načítania príspevku sa vyskytla chyba. Obnovte príspevky a skúste znova. diff --git a/WordPress/src/main/res/values-sq/strings.xml b/WordPress/src/main/res/values-sq/strings.xml index a1c37d0320b0..f175aa4340a0 100644 --- a/WordPress/src/main/res/values-sq/strings.xml +++ b/WordPress/src/main/res/values-sq/strings.xml @@ -3722,7 +3722,6 @@ Language: sq_AL S’arrihet të hiqet kjo etiketë S’arrihet të shtohet kjo etiketë Regjistër aplikacioni - Ndodhi një gabim gjatë krijimit të bazës së të dhënave për aplikacionin. Provoni të riinstaloni aplikacionin. Media s’u rifreskua dot tani Blog WordPress Ndryshime vendore diff --git a/WordPress/src/main/res/values-sr/strings.xml b/WordPress/src/main/res/values-sr/strings.xml index 26be95e63d22..0b4dfa484753 100644 --- a/WordPress/src/main/res/values-sr/strings.xml +++ b/WordPress/src/main/res/values-sr/strings.xml @@ -1685,7 +1685,6 @@ Language: sr_RS Није могуће направити привремену датотеку за отпремање медија. Потврдите да имате довољно слободног простора за складиштење на свом уређају. Погледајте у прегледачу веба Белешка апликације - Дошло је до грешке током прављења базе података апликације. Покушајте поново са инсталацијом апликације. Водоравно поравнање Непожељни коментар Непожељни коментар diff --git a/WordPress/src/main/res/values-sv/strings.xml b/WordPress/src/main/res/values-sv/strings.xml index 6ae8e2b2c3b4..b9dda26cd2c9 100644 --- a/WordPress/src/main/res/values-sv/strings.xml +++ b/WordPress/src/main/res/values-sv/strings.xml @@ -3837,7 +3837,6 @@ Language: sv_SE Kan inte ta bort denna etikett Kan inte lägga till denna etikett Programlogg - Ett fel uppstod vid skapandet av app-databasen. Försök återinstallera appen. Det gick inte att förnya medialistan just nu WordPress-blogg Lokala ändringar diff --git a/WordPress/src/main/res/values-th/strings.xml b/WordPress/src/main/res/values-th/strings.xml index 00dba26bd92f..9777714fa0ff 100644 --- a/WordPress/src/main/res/values-th/strings.xml +++ b/WordPress/src/main/res/values-th/strings.xml @@ -231,7 +231,6 @@ Language: th ใส่ที่อยู่อีเมลที่ใช้งานได้ ไฟล์สื่อใหม่ บล็อกเวิร์ดเพรส - มีความผิดพลาดเกิดขึ้นระหว่างการสร้างฐานข้อมูลแอพ โปรดลองติดตั้งแอพใหม่อีกครั้ง เรื่องใหม่ ดูในเบราว์เซอร์ เพิ่มหมวดหมู่ใหม่ diff --git a/WordPress/src/main/res/values-tr/strings.xml b/WordPress/src/main/res/values-tr/strings.xml index 5bc1e36a173d..d7e6bbfaee8a 100644 --- a/WordPress/src/main/res/values-tr/strings.xml +++ b/WordPress/src/main/res/values-tr/strings.xml @@ -3837,7 +3837,6 @@ Language: tr Bu etiket kaldırılamıyor Bu etiket eklenemiyor Uygulama günlüğü - Uygulama veri tabanı oluşturulurken sorun çıktı. Uygulamayı yeniden kurmayı deneyin. Ortam dosyaları şu anda yenilenemedi WordPress blog Yerel değişiklikler diff --git a/WordPress/src/main/res/values-vi/strings.xml b/WordPress/src/main/res/values-vi/strings.xml index 2230207f1466..90783a17312d 100644 --- a/WordPress/src/main/res/values-vi/strings.xml +++ b/WordPress/src/main/res/values-vi/strings.xml @@ -2613,7 +2613,6 @@ Language: vi_VN Không duyệt Viết bài Tải lên media - Xảy ra lỗi khi tạo cơ sở dữ liệu ứng dụng. Thử cài đặt lại ứng dụng. Nhật ký ứng dụng Thêm chuyên mục mới Tên chuyên mục diff --git a/WordPress/src/main/res/values-zh-rCN/strings.xml b/WordPress/src/main/res/values-zh-rCN/strings.xml index 283c489f2c3c..f0218d1d389a 100644 --- a/WordPress/src/main/res/values-zh-rCN/strings.xml +++ b/WordPress/src/main/res/values-zh-rCN/strings.xml @@ -3830,7 +3830,6 @@ Language: zh_CN 无法删除此标签 无法添加此标签 程序日志 - 创建程序数据库时出错,请重新安装程序. 此时无法刷新媒体 WordPress 博客 本地修改 diff --git a/WordPress/src/main/res/values-zh-rHK/strings.xml b/WordPress/src/main/res/values-zh-rHK/strings.xml index e0406bd24d7c..185bc46c60d1 100644 --- a/WordPress/src/main/res/values-zh-rHK/strings.xml +++ b/WordPress/src/main/res/values-zh-rHK/strings.xml @@ -3817,7 +3817,6 @@ Language: zh_TW 無法移除此標籤 無法新增此標籤 應用程式記錄檔 - 建立應用程式資料庫時發生錯誤。請嘗試重新安裝應用程式。 目前無法重新整理媒體 WordPress 網誌 本機變更 diff --git a/WordPress/src/main/res/values-zh-rTW/strings.xml b/WordPress/src/main/res/values-zh-rTW/strings.xml index e0406bd24d7c..185bc46c60d1 100644 --- a/WordPress/src/main/res/values-zh-rTW/strings.xml +++ b/WordPress/src/main/res/values-zh-rTW/strings.xml @@ -3817,7 +3817,6 @@ Language: zh_TW 無法移除此標籤 無法新增此標籤 應用程式記錄檔 - 建立應用程式資料庫時發生錯誤。請嘗試重新安裝應用程式。 目前無法重新整理媒體 WordPress 網誌 本機變更 diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index e80047d4d379..5cdb2a969f62 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2349,7 +2349,6 @@ Your action is not allowed Internal server error occurred This post no longer exists - An error occurred while creating the app database. Try reinstalling the app. An error occurred while copying text to clipboard Couldn\'t retrieve site info Couldn\'t save site info diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a238a9246ab2..d48d5106edf1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,7 @@ androidx-preference = '1.2.1' androidx-recyclerview = '1.3.2' androidx-room = '2.8.4' androidx-security-crypto = '1.1.0' +androidx-startup = '1.2.0' androidx-swiperefreshlayout = '1.2.0' androidx-test-core = '1.7.0' androidx-test-espresso = '3.7.0' @@ -171,6 +172,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidx-security-crypto" } +androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" } androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } androidx-test-espresso-accessibility = { group = "androidx.test.espresso", name = "espresso-accessibility", version.ref = "androidx-test-espresso" } From a0f23f97bb6aef3aae184cef6568e068a2f5f08f Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Sep 2026 07:12:00 -0400 Subject: [PATCH 2/5] CMM-2174: Address review findings - Make WordPress.wpDB a lazy property; collapse the duplicate catch and remember a failure so a broken database isn't deleted on every access - Add a launchIo helper in AppInitializer so every background step is guarded the same way; use it for the media-sanitize step too - Keep install/upgrade tracking on the main thread so it precedes APPLICATION_OPENED; only the metadata refresh moves to IO - Catch Exception rather than Throwable in the boot receiver - Move the Gravatar initializer lookup into GravatarQuickEditorInitializer, warm it from MeFragment off the main thread, and initialize it before any of the library's exported activities is created --- .../org/wordpress/android/AppInitializer.kt | 55 +++++++++--------- .../java/org/wordpress/android/WordPress.kt | 58 +++++++++++++------ .../wordpress/android/ui/main/MeFragment.kt | 39 +++++++------ .../NotificationsPendingDraftsReceiver.java | 4 +- .../util/GravatarQuickEditorInitializer.kt | 28 +++++++++ 5 files changed, 119 insertions(+), 65 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt diff --git a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt index 7d18ceebb44a..5d4631312f12 100644 --- a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt +++ b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt @@ -435,34 +435,36 @@ class AppInitializer @Inject constructor( } /** - * Enqueues our periodic upload work request, which uploads local drafts or published posts with local - * changes even when the app is closed. Runs off the main thread because the first WorkManager access - * initializes it on demand (see [WordPress.workManagerConfiguration]), which opens its Room database. + * Runs [block] on the IO dispatcher, logging (rather than propagating) any failure. [appScope] has a plain + * [Job], so an uncaught exception would crash the process and cancel every other coroutine in the scope. */ @Suppress("TooGenericExceptionCaught") - private fun enqueuePeriodicUploadWorkAsync() { - periodicUploadEnqueueJob = appScope.launch(Dispatchers.IO) { + private fun launchIo(tag: T, failureMessage: String, block: suspend () -> Unit): Job = + appScope.launch(Dispatchers.IO) { try { - enqueuePeriodicUploadWorkRequestForAllSites() + block() } catch (e: Exception) { - AppLog.e(T.MAIN, "Failed to enqueue periodic upload work", e) + AppLog.e(tag, failureMessage, e) } } + + /** + * Enqueues our periodic upload work request, which uploads local drafts or published posts with local + * changes even when the app is closed. Runs off the main thread because the first WorkManager access + * initializes it on demand (see [WordPress.workManagerConfiguration]), which opens its Room database. + */ + private fun enqueuePeriodicUploadWorkAsync() { + periodicUploadEnqueueJob = launchIo(T.MAIN, "Failed to enqueue periodic upload work") { + enqueuePeriodicUploadWorkRequestForAllSites() + } } /** * Opens the legacy app database off the main thread so that the first main-thread reader usually finds it * ready. [WordPress.wpDB] is lazy and synchronized, so readers that get there first simply open it themselves. */ - @Suppress("TooGenericExceptionCaught") private fun warmUpWpDb() { - appScope.launch(Dispatchers.IO) { - try { - WordPress.wpDB - } catch (e: Exception) { - AppLog.e(T.DB, "Failed to open the app database", e) - } - } + launchIo(T.DB, "Failed to open the app database") { WordPress.wpDB } } @Suppress("TooGenericExceptionCaught") @@ -478,11 +480,11 @@ class AppInitializer @Inject constructor( private fun sanitizeMediaUploadStateForSite() { val selectedSiteLocalId: Int = selectedSiteRepository.getSelectedSiteLocalId(true) - Thread { + launchIo(T.MEDIA, "Failed to sanitize the media upload state") { // The site lookup is a database read (plus credential decryption), so keep it off the main thread - val site = siteStore.getSiteByLocalId(selectedSiteLocalId) ?: return@Thread + val site = siteStore.getSiteByLocalId(selectedSiteLocalId) ?: return@launchIo UploadService.sanitizeMediaUploadStateForSite(mediaStore, dispatcher, site) - }.start() + } } @@ -562,17 +564,14 @@ class AppInitializer @Inject constructor( private fun initAnalytics(elapsedTimeOnCreate: Long) { AnalyticsTracker.registerTracker(tracker) AnalyticsTracker.init(context) + // Tracked synchronously so these always precede APPLICATION_OPENED, which is part of a critical funnel + trackInstallOrUpgrade(elapsedTimeOnCreate) // Refreshing the metadata reads the account and every site from the database, so it runs off the main - // thread. The startup events below stay in the same coroutine, after the refresh, so they are attributed - // to the right user (the tracker only learns the WP.com username from the refresh). - appScope.launch(Dispatchers.IO) { - try { - AnalyticsUtils.refreshMetadata(accountStore, siteStore) - trackInstallOrUpgrade(elapsedTimeOnCreate) - systemNotificationsTracker.checkSystemNotificationsState() - } catch (e: Exception) { - AppLog.e(T.STATS, "Failed to initialize analytics metadata", e) - } + // thread. The notification-state event stays behind it in the same coroutine so it is attributed to + // the right user (the tracker only learns the WP.com username from the refresh). + launchIo(T.STATS, "Failed to initialize analytics metadata") { + AnalyticsUtils.refreshMetadata(accountStore, siteStore) + systemNotificationsTracker.checkSystemNotificationsState() } } diff --git a/WordPress/src/main/java/org/wordpress/android/WordPress.kt b/WordPress/src/main/java/org/wordpress/android/WordPress.kt index 7fe958002977..27ef6e667fd8 100644 --- a/WordPress/src/main/java/org/wordpress/android/WordPress.kt +++ b/WordPress/src/main/java/org/wordpress/android/WordPress.kt @@ -1,10 +1,11 @@ package org.wordpress.android +import android.app.Activity import android.app.Application import android.content.Context import android.database.SQLException -import android.database.sqlite.SQLiteException import android.os.Build +import android.os.Bundle import android.util.Log import androidx.work.Configuration import coil.decode.VideoFrameDecoder @@ -16,6 +17,7 @@ import org.wordpress.android.modules.AppComponent import org.wordpress.android.ui.main.EdgeToEdgeActivityLifecycleCallbacks import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T +import org.wordpress.android.util.GravatarQuickEditorInitializer /** * An abstract class to be extended by {@link WordPressApp} for real application and WordPressTest for UI test @@ -25,8 +27,8 @@ abstract class WordPress : Application(), coil.ImageLoaderFactory, Configuration abstract fun initializer(): AppInitializer /** - * WorkManager is initialized on demand (the default initializer is removed in the manifest) so that - * process starts which never touch WorkManager don't pay for its Room database on the main thread. + * WorkManager is initialized on demand (the default initializer is removed in the manifest) so that its + * Room database is opened by whichever thread first needs it, off the main thread on normal process starts. */ override val workManagerConfiguration: Configuration get() = Configuration.Builder() @@ -42,6 +44,28 @@ abstract class WordPress : Application(), coil.ImageLoaderFactory, Configuration EdgeToEdgeActivityLifecycleCallbacks() ) } + registerActivityLifecycleCallbacks(GravatarQuickEditorActivityInitializer()) + } + + /** + * The Gravatar Quick Editor's startup initializer is removed from the manifest (see + * [GravatarQuickEditorInitializer]). The library also declares exported activities that read the same + * container, so make sure it exists before any of them is created in a fresh process. + */ + private class GravatarQuickEditorActivityInitializer : ActivityLifecycleCallbacks { + override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) { + if (activity.javaClass.name.startsWith(GravatarQuickEditorInitializer.LIBRARY_PACKAGE)) { + GravatarQuickEditorInitializer.initialize(activity) + } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit + override fun onActivityStarted(activity: Activity) = Unit + override fun onActivityResumed(activity: Activity) = Unit + override fun onActivityPaused(activity: Activity) = Unit + override fun onActivityStopped(activity: Activity) = Unit + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit + override fun onActivityDestroyed(activity: Activity) = Unit } fun component(): AppComponent = EntryPoints.get(this, AppComponent::class.java) @@ -89,36 +113,34 @@ abstract class WordPress : Application(), coil.ImageLoaderFactory, Configuration lateinit var versionName: String var appIsInTheBackground = true + // Remembered so a database that can't be opened even after being recreated isn't deleted again on + // every access; the first failure is enough. @Volatile - private var wpDBInstance: WordPressDB? = null + private var wpDBFailure: SQLException? = null /** * The legacy app database. Opened lazily (and warmed up off the main thread by [AppInitializer]) so * that background process starts don't run the SQLite open + migrations on the main thread. */ @JvmStatic - val wpDB: WordPressDB - get() = wpDBInstance ?: synchronized(this) { - wpDBInstance ?: openWpDb(getContext()).also { wpDBInstance = it } - } + val wpDB: WordPressDB by lazy { openWpDb(getContext()) } private fun openWpDb(context: Context): WordPressDB { + wpDBFailure?.let { throw it } return try { WordPressDB(context) - } catch (e: SQLiteException) { - recreateWpDb(context, e) } catch (e: SQLException) { - recreateWpDb(context, e) + AppLog.e(T.DB, "Invalid database, deleting and recreating it", e) + WordPressDB.deleteDatabase(context) + try { + WordPressDB(context) + } catch (retry: SQLException) { + wpDBFailure = retry + throw retry + } } } - private fun recreateWpDb(context: Context, e: Exception): WordPressDB { - AppLog.e(T.DB, e) - AppLog.e(T.DB, "Invalid database, sign out user and delete database") - WordPressDB.deleteDatabase(context) - return WordPressDB(context) - } - @JvmField var requestQueue: RequestQueue? = null diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt index 78b4929bec75..fa3e95c445c9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt @@ -16,8 +16,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.startup.AppInitializer -import androidx.startup.Initializer +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.gravatar.quickeditor.GravatarQuickEditor @@ -30,6 +29,8 @@ import com.gravatar.quickeditor.ui.editor.GravatarQuickEditorParams import com.gravatar.quickeditor.ui.editor.QuickEditorScopeOption import com.gravatar.types.Email import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -63,6 +64,7 @@ import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.util.GravatarQuickEditorInitializer import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T.MAIN import org.wordpress.android.util.FluxCUtils @@ -152,6 +154,23 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { setupViews() setupObservers(savedInstanceState) } + warmUpGravatarQuickEditor() + } + + /** + * Initializes the Gravatar Quick Editor off the main thread so the KeyStore work is already done by the + * time the user taps the avatar. [showQuickEditor] still calls the (idempotent) initializer as a guard. + */ + @Suppress("TooGenericExceptionCaught") + private fun warmUpGravatarQuickEditor() { + val appContext = requireContext().applicationContext + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + try { + GravatarQuickEditorInitializer.initialize(appContext) + } catch (e: Exception) { + appLogWrapper.e(AppLog.T.MAIN, "Gravatar: Failed to initialize the quick editor: ${e.message}") + } + } } @Suppress("LongMethod") @@ -259,7 +278,7 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { ) { try { appLogWrapper.d(AppLog.T.MAIN, "Gravatar: Showing Gravatar quick editor: ${page.value}") - initGravatarQuickEditor() + GravatarQuickEditorInitializer.initialize(requireContext()) GravatarQuickEditor.show( fragment = this@MeFragment, gravatarQuickEditorParams = GravatarQuickEditorParams { @@ -297,18 +316,6 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { } } - /** - * The Gravatar library's androidx.startup initializer is removed from the manifest because it does - * AndroidKeyStore work on the main thread at process start (a source of background ANRs). Initialize it on - * demand instead; [AppInitializer.initializeComponent] is idempotent, so repeated opens are safe. The - * initializer class is internal to the library, hence the lookup by name (R8 keeps every Initializer). - */ - @Suppress("UNCHECKED_CAST") - private fun initGravatarQuickEditor() { - val initializer = Class.forName(GRAVATAR_QUICK_EDITOR_INITIALIZER) as Class> - AppInitializer.getInstance(requireContext()).initializeComponent(initializer) - } - private fun withVeryfiedEmail( @StringRes unverifiedEmailMessageRes: Int, action: () -> Unit, @@ -646,8 +653,6 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { companion object { private const val IS_DISCONNECTING = "IS_DISCONNECTING" private const val GRAVATAR_URL = "https://www.gravatar.com" - private const val GRAVATAR_QUICK_EDITOR_INITIALIZER = - "com.gravatar.quickeditor.initializer.QuickEditorContainerInitializer" fun newInstance(): MeFragment { return MeFragment() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java index 5150373bd2aa..d6825ced9653 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java @@ -60,8 +60,8 @@ public void onReceive(Context context, Intent intent) { new Thread(() -> { try { handleIntent(context, intent); - } catch (Throwable t) { - AppLog.e(AppLog.T.NOTIFS, "Pending Drafts Receiver failed", t); + } catch (Exception e) { + AppLog.e(AppLog.T.NOTIFS, "Pending Drafts Receiver failed", e); } finally { pendingResult.finish(); } diff --git a/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt b/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt new file mode 100644 index 000000000000..d763fb254568 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt @@ -0,0 +1,28 @@ +package org.wordpress.android.util + +import android.content.Context +import androidx.startup.AppInitializer +import androidx.startup.Initializer + +/** + * Initializes the Gravatar Quick Editor on demand. + * + * The library registers an androidx.startup initializer that builds an encrypted DataStore (AndroidKeyStore + + * Tink self-test) on the main thread before Application.onCreate, which showed up in Play Console as background + * ANRs on every process start. That initializer is removed in the manifest and run from here instead, only when + * the editor is actually about to be used. [AppInitializer.initializeComponent] is idempotent and synchronized, + * so this is safe to call repeatedly and from any thread. + * + * The initializer class is internal to the library, hence the lookup by name; startup-runtime's consumer R8 + * rules keep every [Initializer] and its no-arg constructor. + */ +object GravatarQuickEditorInitializer { + const val LIBRARY_PACKAGE = "com.gravatar.quickeditor." + private const val INITIALIZER_CLASS = LIBRARY_PACKAGE + "initializer.QuickEditorContainerInitializer" + + @Suppress("UNCHECKED_CAST") + fun initialize(context: Context) { + val initializer = Class.forName(INITIALIZER_CLASS) as Class> + AppInitializer.getInstance(context).initializeComponent(initializer) + } +} From ba74f73b85ba64b411e12349713d64e8ba716f0b Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Sep 2026 09:00:45 -0400 Subject: [PATCH 3/5] CMM-2174: Check the broadcast action in onReceive Lint's UnsafeProtectedBroadcastReceiver only looks at onReceive itself, so read the action there and pass it to the background handler. --- .../receivers/NotificationsPendingDraftsReceiver.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java index d6825ced9653..f43337519e41 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java @@ -56,10 +56,11 @@ public void onReceive(Context context, Intent intent) { // Both branches read posts from the database. Doing that on the main thread inside a broadcast // (in particular BOOT_COMPLETED, which starts the process in the background) shows up as ANRs, so // the work is moved to a background thread and the broadcast is kept alive with goAsync(). + final String action = intent.getAction(); final PendingResult pendingResult = goAsync(); new Thread(() -> { try { - handleIntent(context, intent); + handleIntent(context, intent, action); } catch (Exception e) { AppLog.e(AppLog.T.NOTIFS, "Pending Drafts Receiver failed", e); } finally { @@ -68,10 +69,9 @@ public void onReceive(Context context, Intent intent) { }, "PendingDraftsReceiver").start(); } - private void handleIntent(Context context, Intent intent) { + private void handleIntent(Context context, Intent intent, String action) { // for the case of being spanned after device restarts, get the latest drafts // and check the lastUpdated - String action = intent.getAction(); if (action != null && action.equals("android.intent.action.BOOT_COMPLETED")) { AppLog.i(AppLog.T.NOTIFS, "entering Pending Drafts Receiver from BOOT_COMPLETED"); // build notifications for existing local drafts From deb83066678f1f30dd44f2c0e138c32b3b033e57 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Sep 2026 17:45:22 -0400 Subject: [PATCH 4/5] CMM-2174: Address second-round review findings - Restore trunk's synchronous analytics init; moving the metadata refresh off main kept surfacing attribution gaps for background-started processes, so that stays as-is until the tracker persists the username - MeFragment: replace the warm-up/guard pair with one Deferred started in onCreate; retry the init once on tap if the warm-up failed; skip showing if the view is no longer started - Catch LinkageError in GravatarQuickEditorInitializer; add an explicit keep rule so the by-name lookup doesn't depend on library rules - Remove the two unused exported Gravatar activities from the manifest instead of initializing the container for them at runtime - LocalNotificationScheduler resolves WorkManager lazily so injecting it no longer initializes WorkManager on the main thread - launchIo uses the injected IO dispatcher; drop the WordPressDB recreate flag; keep the pending-drafts receiver's behaviour unchanged --- WordPress/proguard.cfg | 6 + WordPress/src/main/AndroidManifest.xml | 10 ++ .../org/wordpress/android/AppInitializer.kt | 42 +++---- .../java/org/wordpress/android/WordPress.kt | 38 +------ .../wordpress/android/ui/main/MeFragment.kt | 106 +++++++++--------- .../NotificationsPendingDraftsReceiver.java | 43 ++++--- .../util/GravatarQuickEditorInitializer.kt | 27 ++++- .../local/LocalNotificationScheduler.kt | 7 +- 8 files changed, 136 insertions(+), 143 deletions(-) diff --git a/WordPress/proguard.cfg b/WordPress/proguard.cfg index c408aff90ed8..3843ae7ea7af 100644 --- a/WordPress/proguard.cfg +++ b/WordPress/proguard.cfg @@ -153,3 +153,9 @@ -dontwarn com.google.re2j.Matcher -dontwarn com.google.re2j.Pattern ###### jsoup - end + +###### Gravatar Quick Editor - begin +# The library's startup initializer is removed from the manifest and run on demand by class name (see +# GravatarQuickEditorInitializer), so keep it even if startup-runtime's own consumer rules ever change. +-keep class com.gravatar.quickeditor.initializer.QuickEditorContainerInitializer { (); } +###### Gravatar Quick Editor - end diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 375e717a27b6..955173a0f633 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -1398,6 +1398,16 @@ tools:node="remove" /> + + + + Unit): Job = - appScope.launch(Dispatchers.IO) { + appScope.launch(ioDispatcher) { try { block() } catch (e: Exception) { @@ -564,18 +565,9 @@ class AppInitializer @Inject constructor( private fun initAnalytics(elapsedTimeOnCreate: Long) { AnalyticsTracker.registerTracker(tracker) AnalyticsTracker.init(context) - // Tracked synchronously so these always precede APPLICATION_OPENED, which is part of a critical funnel - trackInstallOrUpgrade(elapsedTimeOnCreate) - // Refreshing the metadata reads the account and every site from the database, so it runs off the main - // thread. The notification-state event stays behind it in the same coroutine so it is attributed to - // the right user (the tracker only learns the WP.com username from the refresh). - launchIo(T.STATS, "Failed to initialize analytics metadata") { - AnalyticsUtils.refreshMetadata(accountStore, siteStore) - systemNotificationsTracker.checkSystemNotificationsState() - } - } + AnalyticsUtils.refreshMetadata(accountStore, siteStore) - private fun trackInstallOrUpgrade(elapsedTimeOnCreate: Long) { + // Track app upgrade and install val versionCode = PackageUtils.getVersionCode(context) val oldVersionCode = AppPrefs.getLastAppVersionCode() if (oldVersionCode == 0) { diff --git a/WordPress/src/main/java/org/wordpress/android/WordPress.kt b/WordPress/src/main/java/org/wordpress/android/WordPress.kt index 27ef6e667fd8..d6811cfdf8da 100644 --- a/WordPress/src/main/java/org/wordpress/android/WordPress.kt +++ b/WordPress/src/main/java/org/wordpress/android/WordPress.kt @@ -1,11 +1,9 @@ package org.wordpress.android -import android.app.Activity import android.app.Application import android.content.Context import android.database.SQLException import android.os.Build -import android.os.Bundle import android.util.Log import androidx.work.Configuration import coil.decode.VideoFrameDecoder @@ -17,7 +15,6 @@ import org.wordpress.android.modules.AppComponent import org.wordpress.android.ui.main.EdgeToEdgeActivityLifecycleCallbacks import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T -import org.wordpress.android.util.GravatarQuickEditorInitializer /** * An abstract class to be extended by {@link WordPressApp} for real application and WordPressTest for UI test @@ -44,28 +41,6 @@ abstract class WordPress : Application(), coil.ImageLoaderFactory, Configuration EdgeToEdgeActivityLifecycleCallbacks() ) } - registerActivityLifecycleCallbacks(GravatarQuickEditorActivityInitializer()) - } - - /** - * The Gravatar Quick Editor's startup initializer is removed from the manifest (see - * [GravatarQuickEditorInitializer]). The library also declares exported activities that read the same - * container, so make sure it exists before any of them is created in a fresh process. - */ - private class GravatarQuickEditorActivityInitializer : ActivityLifecycleCallbacks { - override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) { - if (activity.javaClass.name.startsWith(GravatarQuickEditorInitializer.LIBRARY_PACKAGE)) { - GravatarQuickEditorInitializer.initialize(activity) - } - } - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit - override fun onActivityStarted(activity: Activity) = Unit - override fun onActivityResumed(activity: Activity) = Unit - override fun onActivityPaused(activity: Activity) = Unit - override fun onActivityStopped(activity: Activity) = Unit - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit - override fun onActivityDestroyed(activity: Activity) = Unit } fun component(): AppComponent = EntryPoints.get(this, AppComponent::class.java) @@ -113,11 +88,6 @@ abstract class WordPress : Application(), coil.ImageLoaderFactory, Configuration lateinit var versionName: String var appIsInTheBackground = true - // Remembered so a database that can't be opened even after being recreated isn't deleted again on - // every access; the first failure is enough. - @Volatile - private var wpDBFailure: SQLException? = null - /** * The legacy app database. Opened lazily (and warmed up off the main thread by [AppInitializer]) so * that background process starts don't run the SQLite open + migrations on the main thread. @@ -126,18 +96,12 @@ abstract class WordPress : Application(), coil.ImageLoaderFactory, Configuration val wpDB: WordPressDB by lazy { openWpDb(getContext()) } private fun openWpDb(context: Context): WordPressDB { - wpDBFailure?.let { throw it } return try { WordPressDB(context) } catch (e: SQLException) { AppLog.e(T.DB, "Invalid database, deleting and recreating it", e) WordPressDB.deleteDatabase(context) - try { - WordPressDB(context) - } catch (retry: SQLException) { - wpDBFailure = retry - throw retry - } + WordPressDB(context) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt index fa3e95c445c9..414b42831324 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.collectAsState import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -29,8 +30,11 @@ import com.gravatar.quickeditor.ui.editor.GravatarQuickEditorParams import com.gravatar.quickeditor.ui.editor.QuickEditorScopeOption import com.gravatar.types.Email import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -131,6 +135,9 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { @Inject lateinit var appLogWrapper: AppLogWrapper + // Started in onCreate so the KeyStore-backed Gravatar init is usually finished before the avatar is tapped + private lateinit var quickEditorReady: Deferred + @Inject lateinit var activityNavigator: ActivityNavigator @@ -146,6 +153,10 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (requireActivity().application as WordPress).component().inject(this) + val appContext = requireContext().applicationContext + quickEditorReady = lifecycleScope.async(Dispatchers.IO) { + GravatarQuickEditorInitializer.initialize(appContext) + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -154,23 +165,6 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { setupViews() setupObservers(savedInstanceState) } - warmUpGravatarQuickEditor() - } - - /** - * Initializes the Gravatar Quick Editor off the main thread so the KeyStore work is already done by the - * time the user taps the avatar. [showQuickEditor] still calls the (idempotent) initializer as a guard. - */ - @Suppress("TooGenericExceptionCaught") - private fun warmUpGravatarQuickEditor() { - val appContext = requireContext().applicationContext - viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { - try { - GravatarQuickEditorInitializer.initialize(appContext) - } catch (e: Exception) { - appLogWrapper.e(AppLog.T.MAIN, "Gravatar: Failed to initialize the quick editor: ${e.message}") - } - } } @Suppress("LongMethod") @@ -276,43 +270,53 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { private fun MeFragmentBinding.showQuickEditor( page: AvatarPickerAndAboutEditorConfiguration.Page ) { - try { - appLogWrapper.d(AppLog.T.MAIN, "Gravatar: Showing Gravatar quick editor: ${page.value}") - GravatarQuickEditorInitializer.initialize(requireContext()) - GravatarQuickEditor.show( - fragment = this@MeFragment, - gravatarQuickEditorParams = GravatarQuickEditorParams { - email = Email(accountStore.account.email) - scopeOption = QuickEditorScopeOption.avatarAndAbout { - initialPage = page - fields = setOf( - AboutInputField.FirstName, - AboutInputField.LastName, - AboutInputField.DisplayName, - AboutInputField.AboutMe - ) - } - }, - authenticationMethod = AuthenticationMethod.Bearer(accountStore.accessToken.orEmpty()), - updateHandler = { event -> - when (event) { - AvatarPickerResult -> { - loadAvatar(null, true) + appLogWrapper.d(AppLog.T.MAIN, "Gravatar: Showing Gravatar quick editor: ${page.value}") + val appContext = requireContext().applicationContext + viewLifecycleOwner.lifecycleScope.launch { + // Complete except in the first moments of the fragment's life, so this almost never suspends. A + // warm-up that failed (e.g. a transient KeyStore error) gets one more try here. + val ready = quickEditorReady.await() || + withContext(Dispatchers.IO) { GravatarQuickEditorInitializer.initialize(appContext) } + if (!ready) return@launch + // A tap that did suspend could resume after the fragment left the screen + if (!viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) return@launch + try { + GravatarQuickEditor.show( + fragment = this@MeFragment, + gravatarQuickEditorParams = GravatarQuickEditorParams { + email = Email(accountStore.account.email) + scopeOption = QuickEditorScopeOption.avatarAndAbout { + initialPage = page + fields = setOf( + AboutInputField.FirstName, + AboutInputField.LastName, + AboutInputField.DisplayName, + AboutInputField.AboutMe + ) } - - is AboutEditorResult -> { - meDisplayName.text = event.profile.displayName.ifEmpty { accountStore.account.displayName } + }, + authenticationMethod = AuthenticationMethod.Bearer(accountStore.accessToken.orEmpty()), + updateHandler = { event -> + when (event) { + AvatarPickerResult -> { + loadAvatar(null, true) + } + + is AboutEditorResult -> { + meDisplayName.text = + event.profile.displayName.ifEmpty { accountStore.account.displayName } + } + + else -> Unit } - - else -> Unit + }, + onDismiss = { + appLogWrapper.d(AppLog.T.MAIN, "Gravatar: Gravatar quick editor dismissed: ${page.value}") } - }, - onDismiss = { - appLogWrapper.d(AppLog.T.MAIN, "Gravatar: Gravatar quick editor dismissed: ${page.value}") - } - ) - } catch (exception: Exception) { - appLogWrapper.d(AppLog.T.MAIN, "Gravatar: Error opening Gravatar quick editor: ${exception.message}") + ) + } catch (exception: Exception) { + appLogWrapper.d(AppLog.T.MAIN, "Gravatar: Error opening Gravatar quick editor: ${exception.message}") + } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java index f43337519e41..45b0e1a91041 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java @@ -56,11 +56,18 @@ public void onReceive(Context context, Intent intent) { // Both branches read posts from the database. Doing that on the main thread inside a broadcast // (in particular BOOT_COMPLETED, which starts the process in the background) shows up as ANRs, so // the work is moved to a background thread and the broadcast is kept alive with goAsync(). - final String action = intent.getAction(); + final boolean isBootCompleted = Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()); + final int postId = intent.getIntExtra(POST_ID_EXTRA, 0); final PendingResult pendingResult = goAsync(); new Thread(() -> { try { - handleIntent(context, intent, action); + if (isBootCompleted) { + AppLog.i(AppLog.T.NOTIFS, "entering Pending Drafts Receiver from BOOT_COMPLETED"); + rescheduleNotificationsForLocalDrafts(context); + } else { + AppLog.i(AppLog.T.NOTIFS, "entering Pending Drafts Receiver from alarm"); + buildNotificationForPostId(postId, context); + } } catch (Exception e) { AppLog.e(AppLog.T.NOTIFS, "Pending Drafts Receiver failed", e); } finally { @@ -69,26 +76,18 @@ public void onReceive(Context context, Intent intent) { }, "PendingDraftsReceiver").start(); } - private void handleIntent(Context context, Intent intent, String action) { - // for the case of being spanned after device restarts, get the latest drafts - // and check the lastUpdated - if (action != null && action.equals("android.intent.action.BOOT_COMPLETED")) { - AppLog.i(AppLog.T.NOTIFS, "entering Pending Drafts Receiver from BOOT_COMPLETED"); - // build notifications for existing local drafts - SiteModel site = mSiteStore.getSiteByLocalId(mSelectedSiteRepository.getSelectedSiteLocalId()); - if (site != null) { - List draftPosts = mPostStore.getPostsForSite(site); - for (PostModel post : draftPosts) { - // reschedule next notifications for each local draft post we have, as we have - // just been rebooted - PendingDraftsNotificationsUtils.scheduleNextNotifications(context, post.getId(), - post.getDateLocallyChanged()); - } - } - } else { - AppLog.i(AppLog.T.NOTIFS, "entering Pending Drafts Receiver from alarm"); - // get extras from intent in order to build notification - buildNotificationForPostId(intent.getIntExtra(POST_ID_EXTRA, 0), context); + /** + * After a reboot every alarm is gone, so reschedule the next notification for each local draft. + */ + private void rescheduleNotificationsForLocalDrafts(Context context) { + SiteModel site = mSiteStore.getSiteByLocalId(mSelectedSiteRepository.getSelectedSiteLocalId()); + if (site == null) { + return; + } + List draftPosts = mPostStore.getPostsForSite(site); + for (PostModel post : draftPosts) { + PendingDraftsNotificationsUtils.scheduleNextNotifications(context, post.getId(), + post.getDateLocallyChanged()); } } diff --git a/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt b/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt index d763fb254568..d1666c6a96a5 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt @@ -3,6 +3,7 @@ package org.wordpress.android.util import android.content.Context import androidx.startup.AppInitializer import androidx.startup.Initializer +import org.wordpress.android.util.AppLog.T /** * Initializes the Gravatar Quick Editor on demand. @@ -13,16 +14,30 @@ import androidx.startup.Initializer * the editor is actually about to be used. [AppInitializer.initializeComponent] is idempotent and synchronized, * so this is safe to call repeatedly and from any thread. * - * The initializer class is internal to the library, hence the lookup by name; startup-runtime's consumer R8 - * rules keep every [Initializer] and its no-arg constructor. + * The initializer class is internal to the library, hence the lookup by name. WordPress/proguard.cfg keeps the + * class and its no-arg constructor so the lookup survives R8. + * + * TODO: remove this (and the manifest override) once gravatar-android builds its container lazily or exposes a + * public initialize(context) entry point. */ object GravatarQuickEditorInitializer { - const val LIBRARY_PACKAGE = "com.gravatar.quickeditor." - private const val INITIALIZER_CLASS = LIBRARY_PACKAGE + "initializer.QuickEditorContainerInitializer" + private const val INITIALIZER_CLASS = "com.gravatar.quickeditor.initializer.QuickEditorContainerInitializer" - @Suppress("UNCHECKED_CAST") - fun initialize(context: Context) { + /** + * Runs the library initializer, returning false (after logging) if it could not be run. Callers must not + * open the editor in that case, since it would throw on its uninitialized container. + */ + @Suppress("UNCHECKED_CAST", "TooGenericExceptionCaught") + fun initialize(context: Context): Boolean = try { val initializer = Class.forName(INITIALIZER_CLASS) as Class> AppInitializer.getInstance(context).initializeComponent(initializer) + true + } catch (e: Exception) { + AppLog.e(T.MAIN, "Failed to initialize the Gravatar quick editor", e) + false + } catch (e: LinkageError) { + // e.g. NoClassDefFoundError if a library bump moves the class; an Error, so not covered above + AppLog.e(T.MAIN, "Failed to initialize the Gravatar quick editor", e) + false } } diff --git a/WordPress/src/main/java/org/wordpress/android/workers/notification/local/LocalNotificationScheduler.kt b/WordPress/src/main/java/org/wordpress/android/workers/notification/local/LocalNotificationScheduler.kt index c9aa4809f56c..dd0df2fe681c 100644 --- a/WordPress/src/main/java/org/wordpress/android/workers/notification/local/LocalNotificationScheduler.kt +++ b/WordPress/src/main/java/org/wordpress/android/workers/notification/local/LocalNotificationScheduler.kt @@ -6,9 +6,12 @@ import androidx.work.WorkManager import org.wordpress.android.workers.notification.local.LocalNotification.Type import javax.inject.Inject -class LocalNotificationScheduler(private val workManager: WorkManager) { +class LocalNotificationScheduler(workManagerProvider: () -> WorkManager) { @Inject - constructor(context: Context) : this(WorkManager.getInstance(context)) + constructor(context: Context) : this({ WorkManager.getInstance(context) }) + + // Resolved on first use so that injecting this class doesn't initialize WorkManager on the main thread + private val workManager by lazy(workManagerProvider) fun scheduleOneTimeNotification(vararg localNotifications: LocalNotification) { workManager.enqueue(localNotifications.map { buildOneTimeWorkRequest(it) }) From c88a98e7c1896e9ec1e274aab7afd8d781c710c9 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Sep 2026 18:03:54 -0400 Subject: [PATCH 5/5] CMM-2174: Reword a KDoc note that tripped detekt's ForbiddenComment rule --- .../wordpress/android/util/GravatarQuickEditorInitializer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt b/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt index d1666c6a96a5..73912799143c 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/GravatarQuickEditorInitializer.kt @@ -17,8 +17,8 @@ import org.wordpress.android.util.AppLog.T * The initializer class is internal to the library, hence the lookup by name. WordPress/proguard.cfg keeps the * class and its no-arg constructor so the lookup survives R8. * - * TODO: remove this (and the manifest override) once gravatar-android builds its container lazily or exposes a - * public initialize(context) entry point. + * Remove this (and the manifest override) once gravatar-android builds its container lazily or exposes a + * public initialize(context) entry point. */ object GravatarQuickEditorInitializer { private const val INITIALIZER_CLASS = "com.gravatar.quickeditor.initializer.QuickEditorContainerInitializer"