Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions WordPress/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions WordPress/proguard.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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 { <init>(); }
###### Gravatar Quick Editor - end
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1389,8 +1389,25 @@
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
<!-- The Gravatar Quick Editor initializer builds an encrypted DataStore (AndroidKeyStore + Tink
self-test) on the main thread before Application.onCreate, which shows up as background ANRs.
It is initialized on demand from MeFragment instead. -->
<meta-data
android:name="com.gravatar.quickeditor.initializer.QuickEditorContainerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>

<!-- The Gravatar Quick Editor library exports two activities the app never launches (it shows the editor
as a sheet inside the host activity). They would crash without the initializer removed above, so
drop them rather than leave an exported entry point around. -->
<activity
android:name="com.gravatar.quickeditor.ui.GravatarQuickEditorActivity"
tools:node="remove" />
<activity
android:name="com.gravatar.quickeditor.ui.oauth.GravatarOAuthActivity"
tools:node="remove" />

<activity android:name=".ui.blaze.blazepromote.BlazePromoteParentActivity"
android:exported="false"
android:label="@string/blaze_activity_title"
Expand Down
124 changes: 58 additions & 66 deletions WordPress/src/main/java/org/wordpress/android/AppInitializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,26 @@ import android.app.SyncNotedAppOp
import android.content.ComponentCallbacks2
import android.content.Context
import android.content.res.Configuration
import android.database.SQLException
import android.database.sqlite.SQLiteException
import android.net.http.HttpResponseCache
import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.SystemClock
import android.text.TextUtils
import android.util.Log
import android.webkit.WebView
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import com.android.volley.RequestQueue
import com.automattic.android.tracks.crashlogging.CrashLogging
import com.google.firebase.iid.FirebaseInstanceId
import com.wordpress.rest.RestClient
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
Expand Down Expand Up @@ -64,6 +62,7 @@ import org.wordpress.android.fluxc.store.StatsStore
import org.wordpress.android.fluxc.tools.FluxCImageLoader
import org.wordpress.android.fluxc.utils.ErrorUtils.OnUnexpectedError
import org.wordpress.android.modules.APPLICATION_SCOPE
import org.wordpress.android.modules.IO_THREAD
import org.wordpress.android.networking.NetworkConnectionMonitor
import org.wordpress.android.networking.OAuthAuthenticator
import org.wordpress.android.networking.RestClientUtils
Expand Down Expand Up @@ -109,7 +108,6 @@ import org.wordpress.android.util.config.OpenWebLinksWithJetpackFlowFeatureConfi
import org.wordpress.android.util.enqueuePeriodicUploadWorkRequestForAllSites
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.widgets.AppReviewManager
import org.wordpress.android.workers.WordPressWorkersFactory
import java.io.File
import java.io.IOException
import java.net.CookieManager
Expand Down Expand Up @@ -184,9 +182,6 @@ class AppInitializer @Inject constructor(
@Inject
lateinit var imageEditorFileUtils: ImageEditorFileUtils

@Inject
lateinit var wordPressWorkerFactory: WordPressWorkersFactory

@Inject
lateinit var gcmRegistrationScheduler: GCMRegistrationScheduler

Expand All @@ -202,6 +197,10 @@ class AppInitializer @Inject constructor(
@Named(APPLICATION_SCOPE)
lateinit var appScope: CoroutineScope

@Inject
@Named(IO_THREAD)
lateinit var ioDispatcher: CoroutineDispatcher

@Inject
lateinit var selectedSiteRepository: SelectedSiteRepository

Expand Down Expand Up @@ -246,6 +245,13 @@ class AppInitializer @Inject constructor(

private lateinit var applicationLifecycleMonitor: ApplicationLifecycleMonitor

/**
* The coroutine enqueuing the periodic upload work. Exposed so UI tests can wait for it before swapping in
* a test WorkManager.
*/
@VisibleForTesting
var periodicUploadEnqueueJob: Job? = null
private set

private var startDate: Long

Expand Down Expand Up @@ -311,7 +317,7 @@ class AppInitializer @Inject constructor(
AppLog.i(T.UTILS, "AppInitializer.init")

WordPress.versionName = PackageUtils.getVersionName(application)
initWpDb()
warmUpWpDb()
context?.let { enableHttpResponseCache(it) }

AppReviewManager.init(application)
Expand Down Expand Up @@ -362,15 +368,10 @@ class AppInitializer @Inject constructor(
// remove expired lists
dispatcher.dispatch(ListActionBuilder.newRemoveExpiredListsAction(RemoveExpiredListsPayload()))


if (!initialized) {
initWorkManager()
enqueuePeriodicUploadWorkAsync()
}

// Enqueue our periodic upload work request. The UploadWorkRequest will be called even if the app is closed.
// It will upload local draft or published posts with local changes to the server.
enqueuePeriodicUploadWorkRequestForAllSites()

systemNotificationsTracker.checkSystemNotificationsState()
ImageEditorInitializer.init(imageManager, imageEditorTracker, imageEditorFileUtils, appScope)

Expand All @@ -394,19 +395,14 @@ class AppInitializer @Inject constructor(
// when called from Application.onCreate(). The Help screen is the only entry
// point that needs Zendesk and is gated by user navigation, so deferring init
// to a background coroutine is safe in practice.
@Suppress("TooGenericExceptionCaught")
private fun initZendeskAsync() {
appScope.launch(Dispatchers.IO) {
try {
zendeskHelper.setupZendesk(
application,
BuildConfig.ZENDESK_DOMAIN,
BuildConfig.ZENDESK_APP_ID,
BuildConfig.ZENDESK_OAUTH_CLIENT_ID
)
} catch (e: Exception) {
AppLog.e(T.SUPPORT, "Failed to initialize Zendesk SDK", e)
}
launchIo(T.SUPPORT, "Failed to initialize Zendesk SDK") {
zendeskHelper.setupZendesk(
application,
BuildConfig.ZENDESK_DOMAIN,
BuildConfig.ZENDESK_APP_ID,
BuildConfig.ZENDESK_OAUTH_CLIENT_ID
)
}
}

Expand Down Expand Up @@ -439,13 +435,37 @@ class AppInitializer @Inject constructor(
appOpsManager.setOnOpNotedCallback(context?.mainExecutor, appOpsCallback)
}

private fun initWorkManager() {
val configBuilder = androidx.work.Configuration.Builder().setWorkerFactory(wordPressWorkerFactory)
if (BuildConfig.DEBUG) {
configBuilder.setMinimumLoggingLevel(Log.DEBUG)
/**
* 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 launchIo(tag: T, failureMessage: String, block: suspend () -> Unit): Job =
appScope.launch(ioDispatcher) {
try {
block()
} catch (e: Exception) {
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()
}
configBuilder.setJobSchedulerJobIdRange(WORK_MANAGER_ID_RANGE_MIN, WORK_MANAGER_ID_RANGE_MAX)
WorkManager.initialize(application, configBuilder.build())
}

/**
* 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.
*/
private fun warmUpWpDb() {
launchIo(T.DB, "Failed to open the app database") { WordPress.wpDB }
}

@Suppress("TooGenericExceptionCaught")
Expand All @@ -461,11 +481,10 @@ class AppInitializer @Inject constructor(

private fun sanitizeMediaUploadStateForSite() {
val selectedSiteLocalId: Int = selectedSiteRepository.getSelectedSiteLocalId(true)
val site = siteStore.getSiteByLocalId(selectedSiteLocalId)
site?.let {
Thread {
UploadService.sanitizeMediaUploadStateForSite(mediaStore, dispatcher, site)
}.start()
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@launchIo
UploadService.sanitizeMediaUploadStateForSite(mediaStore, dispatcher, site)
}
}

Expand Down Expand Up @@ -584,28 +603,6 @@ class AppInitializer @Inject constructor(
}
}

private fun initWpDb() {
if (!createAndVerifyWpDb()) {
AppLog.e(T.DB, "Invalid database, sign out user and delete database")
// Force DB deletion
WordPressDB.deleteDatabase(application)
WordPress.wpDB = WordPressDB(application)
}
}

private fun createAndVerifyWpDb(): Boolean {
return try {
WordPress.wpDB = WordPressDB(application)
true
} catch (e: SQLiteException) {
AppLog.e(T.DB, e)
false
} catch (e: SQLException) {
AppLog.e(T.DB, e)
false
}
}

/**
* Sign out from wpcom account.
* Note: This method must not be called on UI Thread.
Expand Down Expand Up @@ -966,11 +963,6 @@ class AppInitializer @Inject constructor(
private const val MEMORY_CACHE_RATIO = 0.25 // Use 1/4th of the available memory for memory cache.
private const val DEFAULT_TIMEOUT = 2 * 60 // 2 minutes

// 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

@SuppressLint("StaticFieldLeak")
var context: Context? = null
private set
Expand Down
Loading
Loading