Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amp 90600 offline support #166

Closed
wants to merge 8 commits into from
Closed
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 android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.amplitude.android">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
61 changes: 49 additions & 12 deletions android/src/main/java/com/amplitude/android/Amplitude.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.amplitude.android

import AndroidNetworkListener
import android.content.Context
import com.amplitude.android.migration.ApiKeyStorageMigration
import com.amplitude.android.migration.RemnantDataMigration
import com.amplitude.android.plugins.AnalyticsConnectorIdentityPlugin
import com.amplitude.android.plugins.AnalyticsConnectorPlugin
import com.amplitude.android.plugins.AndroidContextPlugin
import com.amplitude.android.plugins.AndroidLifecyclePlugin
import com.amplitude.android.utilities.AndroidNetworkConnectivityChecker
import com.amplitude.core.Amplitude
import com.amplitude.core.events.BaseEvent
import com.amplitude.core.platform.plugins.AmplitudeDestination
Expand All @@ -16,11 +18,21 @@ import com.amplitude.id.IdentityConfiguration
import kotlinx.coroutines.launch

open class Amplitude(
configuration: Configuration
) : Amplitude(configuration) {

configuration: Configuration,
) : Amplitude(configuration), AndroidNetworkListener.NetworkChangeCallback {
internal var inForeground = false
private lateinit var androidContextPlugin: AndroidContextPlugin
private var networkListener: AndroidNetworkListener
private val networkChangeHandler =
object : AndroidNetworkListener.NetworkChangeCallback {
override fun onNetworkAvailable() {
flush()
}

override fun onNetworkUnavailable() {
// Nothing to do so far
}
}

val sessionId: Long
get() {
Expand All @@ -29,6 +41,9 @@ open class Amplitude(

init {
registerShutdownHook()
networkListener = AndroidNetworkListener((this.configuration as Configuration).context)
networkListener.setNetworkChangeCallback(networkChangeHandler)
networkListener.startListening()
}

override fun createTimeline(): Timeline {
Expand All @@ -37,14 +52,18 @@ open class Amplitude(

override fun createIdentityConfiguration(): IdentityConfiguration {
val configuration = configuration as Configuration
val storageDirectory = configuration.context.getDir("${FileStorage.STORAGE_PREFIX}-${configuration.instanceName}", Context.MODE_PRIVATE)
val storageDirectory =
configuration.context.getDir(
"${FileStorage.STORAGE_PREFIX}-${configuration.instanceName}",
Context.MODE_PRIVATE,
)

return IdentityConfiguration(
instanceName = configuration.instanceName,
apiKey = configuration.apiKey,
identityStorageProvider = configuration.identityStorageProvider,
storageDirectory = storageDirectory,
logger = configuration.loggerProvider.getLogger(this)
logger = configuration.loggerProvider.getLogger(this),
)
}

Expand All @@ -62,7 +81,7 @@ open class Amplitude(
add(AndroidLifecyclePlugin())
add(AnalyticsConnectorIdentityPlugin())
add(AnalyticsConnectorPlugin())
add(AmplitudeDestination())
add(AmplitudeDestination(AndroidNetworkConnectivityChecker(this.configuration.context, this.logger)))

(timeline as Timeline).start()
}
Expand Down Expand Up @@ -113,18 +132,22 @@ open class Amplitude(
}

private fun registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(object : Thread() {
override fun run() {
([email protected] as Timeline).stop()
}
})
Runtime.getRuntime().addShutdownHook(
object : Thread() {
override fun run() {
([email protected] as Timeline).stop()
([email protected] as AndroidNetworkListener).stopListening()
}
},
)
}

companion object {
/**
* The event type for start session events.
*/
const val START_SESSION_EVENT = "session_start"

/**
* The event type for end session events.
*/
Expand All @@ -134,12 +157,22 @@ open class Amplitude(
* The event type for dummy enter foreground events.
*/
internal const val DUMMY_ENTER_FOREGROUND_EVENT = "dummy_enter_foreground"

/**
* The event type for dummy exit foreground events.
*/
internal const val DUMMY_EXIT_FOREGROUND_EVENT = "dummy_exit_foreground"
}

override fun onNetworkAvailable() {
networkChangeHandler.onNetworkAvailable()
}

override fun onNetworkUnavailable() {
networkChangeHandler.onNetworkUnavailable()
}
}

/**
* constructor function to build amplitude in dsl format with config options
* Usage: Amplitude("123", context) {
Expand All @@ -153,7 +186,11 @@ open class Amplitude(
* @param configs Configuration
* @return Amplitude Android Instance
*/
fun Amplitude(apiKey: String, context: Context, configs: Configuration.() -> Unit): com.amplitude.android.Amplitude {
fun Amplitude(
apiKey: String,
context: Context,
configs: Configuration.() -> Unit,
): com.amplitude.android.Amplitude {
val config = Configuration(apiKey, context)
configs.invoke(config)
return com.amplitude.android.Amplitude(config)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.amplitude.android.utilities

import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import com.amplitude.common.Logger
import com.amplitude.core.platform.NetworkConnectivityChecker

class AndroidNetworkConnectivityChecker(private val context: Context, private val logger: Logger) : NetworkConnectivityChecker {
companion object {
private const val ACCESS_NETWORK_STATE = "android.permission.ACCESS_NETWORK_STATE"
}

override suspend fun isConnected(): Boolean {
// Assume connection and proceed.
// Events will be treated like online
// regardless network connectivity
if (!hasPermission(context, ACCESS_NETWORK_STATE)) {
logger.warn("No ACCESS_NETWORK_STATE permission, offline mode is not supported.")
return true
}

val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = cm.activeNetwork ?: return false
val capabilities = cm.getNetworkCapabilities(network) ?: return false

return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
} else {
@SuppressLint("MissingPermission")
val networkInfo = cm.activeNetworkInfo
return networkInfo != null && networkInfo.isConnectedOrConnecting
}
}

private fun hasPermission(
context: Context,
permission: String,
): Boolean {
return context.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import java.lang.IllegalArgumentException

class AndroidNetworkListener(private val context: Context) {
private var networkCallback: NetworkChangeCallback? = null
private var networkCallbackForLowerApiLevels: BroadcastReceiver? = null
private var networkCallbackForHigherApiLevels: ConnectivityManager.NetworkCallback? = null

interface NetworkChangeCallback {
fun onNetworkAvailable()

fun onNetworkUnavailable()
}

fun setNetworkChangeCallback(callback: NetworkChangeCallback) {
this.networkCallback = callback
}

fun startListening() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setupNetworkCallback()
} else {
setupBroadcastReceiver()
}
}

@SuppressLint("NewApi")
private fun setupNetworkCallback() {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkRequest =
NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()

networkCallbackForHigherApiLevels =
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
networkCallback?.onNetworkAvailable()
}

override fun onLost(network: Network) {
networkCallback?.onNetworkUnavailable()
}
}

connectivityManager.registerNetworkCallback(networkRequest, networkCallbackForHigherApiLevels!!)
}

private fun setupBroadcastReceiver() {
networkCallbackForLowerApiLevels =
object : BroadcastReceiver() {
override fun onReceive(
context: Context,
intent: Intent,
) {
if (ConnectivityManager.CONNECTIVITY_ACTION == intent.action) {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = connectivityManager.activeNetworkInfo
val isConnected = activeNetwork?.isConnectedOrConnecting == true

if (isConnected) {
networkCallback?.onNetworkAvailable()
} else {
networkCallback?.onNetworkUnavailable()
}
}
}
}

val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
context.registerReceiver(networkCallbackForLowerApiLevels, filter)
}

fun stopListening() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
networkCallbackForHigherApiLevels?.let { connectivityManager.unregisterNetworkCallback(it) }
} else {
networkCallbackForLowerApiLevels?.let { context.unregisterReceiver(it) }
}
} catch (e: IllegalArgumentException) {
// callback was already unregistered.
} catch (e: IllegalStateException) {
// shutdown process is in progress and certain operations are not allowed.
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.amplitude.android

import android.app.Application
import android.content.Context
import android.net.ConnectivityManager
import com.amplitude.core.events.BaseEvent
import com.amplitude.core.utilities.ConsoleLoggerProvider
import com.amplitude.id.IMIdentityStorageProvider
Expand All @@ -23,15 +24,17 @@ import kotlin.io.path.absolutePathString
class AmplitudeRobolectricTests {
private lateinit var amplitude: Amplitude
private var context: Context? = null
private lateinit var connectivityManager: ConnectivityManager

var tempDir = TempDirectory()

@ExperimentalCoroutinesApi
@Before
fun setup() {
context = mockk<Application>(relaxed = true)
connectivityManager = mockk<ConnectivityManager>(relaxed = true)
every { context!!.getDir(any(), any()) } returns File(tempDir.create("data").absolutePathString())

every { context!!.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager
amplitude = Amplitude(createConfiguration())
}

Expand Down
Loading
Loading