Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.wordpress.android.modules

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.wordpress.android.ui.newstats.datasource.StatsDataSource
import org.wordpress.android.ui.newstats.datasource.StatsDataSourceImpl

@InstallIn(SingletonComponent::class)
@Module
abstract class StatsModule {
@Binds
abstract fun bindStatsDataSource(impl: StatsDataSourceImpl): StatsDataSource
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import kotlinx.coroutines.launch
import org.wordpress.android.R
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.main.BaseAppCompatActivity
import org.wordpress.android.ui.newstats.todaysstat.TodaysStatsCard
import org.wordpress.android.ui.newstats.todaysstat.TodaysStatsViewModel
import org.wordpress.android.ui.newstats.todaysstats.TodaysStatsCard
import org.wordpress.android.ui.newstats.todaysstats.TodaysStatsViewModel
import org.wordpress.android.ui.newstats.viewsstats.ViewsStatsCard
import org.wordpress.android.ui.newstats.viewsstats.ViewsStatsViewModel

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.wordpress.android.ui.newstats.datasource

/**
* Data source interface for fetching stats data.
* This abstraction allows mocking the data layer in tests without needing access to uniffi objects.
*/
interface StatsDataSource {
/**
* Initializes the data source with the access token.
*/
fun init(accessToken: String)

/**
* Fetches stats data for a specific site.
*
* @param siteId The WordPress.com site ID
* @param unit The time unit for the stats (HOUR, DAY, etc.)
* @param quantity The number of data points to fetch
* @param endDate The end date for the stats period (format: yyyy-MM-dd)
* @return Result containing the stats data or an error
*/
suspend fun fetchStatsVisits(
siteId: Long,
unit: StatsUnit,
quantity: Int,
endDate: String
): StatsVisitsDataResult
}

/**
* Time unit for stats data.
*/
enum class StatsUnit {
HOUR,
DAY
}

/**
* Result wrapper for stats visits fetch operation.
*/
sealed class StatsVisitsDataResult {
data class Success(val data: StatsVisitsData) : StatsVisitsDataResult()
data class Error(val message: String) : StatsVisitsDataResult()
}

/**
* Stats visits data from the API.
* Contains all the data points for views, visitors, likes, comments, and posts.
*/
data class StatsVisitsData(
val visits: List<VisitsDataPoint>,
val visitors: List<VisitorsDataPoint>,
val likes: List<LikesDataPoint>,
val comments: List<CommentsDataPoint>,
val posts: List<PostsDataPoint>
)

/**
* Data point for visits/views.
*/
data class VisitsDataPoint(
val period: String,
val visits: Long
)

/**
* Data point for visitors.
*/
data class VisitorsDataPoint(
val period: String,
val visitors: Long
)

/**
* Data point for likes.
*/
data class LikesDataPoint(
val period: String,
val likes: Long
)

/**
* Data point for comments.
*/
data class CommentsDataPoint(
val period: String,
val comments: Long
)

/**
* Data point for posts.
*/
data class PostsDataPoint(
val period: String,
val posts: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.wordpress.android.ui.newstats.datasource

import org.wordpress.android.networking.restapi.WpComApiClientProvider
import org.wordpress.android.ui.newstats.extension.statsCommentsData
import org.wordpress.android.ui.newstats.extension.statsLikesData
import org.wordpress.android.ui.newstats.extension.statsPostsData
import org.wordpress.android.ui.newstats.extension.statsVisitorsData
import org.wordpress.android.ui.newstats.extension.statsVisitsData
import rs.wordpress.api.kotlin.WpComApiClient
import rs.wordpress.api.kotlin.WpRequestResult
import uniffi.wp_api.StatsVisitsParams
import uniffi.wp_api.StatsVisitsUnit
import javax.inject.Inject

/**
* Implementation of [StatsDataSource] that fetches stats data from the WordPress.com API
* using the wordpress-rs library.
*/
class StatsDataSourceImpl @Inject constructor(
private val wpComApiClientProvider: WpComApiClientProvider
) : StatsDataSource {
/**
* Access token for API authentication.
* Marked as @Volatile to ensure visibility across threads since this data source is accessed
* from multiple coroutine contexts.
*/
@Volatile
private var accessToken: String? = null

private val wpComApiClient: WpComApiClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
check(accessToken != null) { "DataSource not initialized" }
wpComApiClientProvider.getWpComApiClient(accessToken!!)
}

override fun init(accessToken: String) {
this.accessToken = accessToken
}

override suspend fun fetchStatsVisits(
siteId: Long,
unit: StatsUnit,
quantity: Int,
endDate: String
): StatsVisitsDataResult {
val params = StatsVisitsParams(
unit = unit.toApiUnit(),
quantity = quantity.toUInt(),
endDate = endDate
)

val result = wpComApiClient.request { requestBuilder ->
requestBuilder.statsVisits().getStatsVisits(
wpComSiteId = siteId.toULong(),
params = params
)
}

return when (result) {
is WpRequestResult.Success -> {
StatsVisitsDataResult.Success(mapToStatsVisitsData(result.response.data))
}
is WpRequestResult.WpError -> {
StatsVisitsDataResult.Error(result.errorMessage)
}
else -> {
StatsVisitsDataResult.Error("Unknown error")
}
}
}

private fun mapToStatsVisitsData(response: uniffi.wp_api.StatsVisitsResponse): StatsVisitsData {
return StatsVisitsData(
visits = response.statsVisitsData().map { dataPoint ->
VisitsDataPoint(period = dataPoint.period, visits = dataPoint.visits.toLong())
},
visitors = response.statsVisitorsData().map { dataPoint ->
VisitorsDataPoint(period = dataPoint.period, visitors = dataPoint.visitors.toLong())
},
likes = response.statsLikesData().map { dataPoint ->
LikesDataPoint(period = dataPoint.period, likes = dataPoint.likes.toLong())
},
comments = response.statsCommentsData().map { dataPoint ->
CommentsDataPoint(period = dataPoint.period, comments = dataPoint.comments.toLong())
},
posts = response.statsPostsData().map { dataPoint ->
PostsDataPoint(period = dataPoint.period, posts = dataPoint.posts.toLong())
}
)
}

private fun StatsUnit.toApiUnit(): StatsVisitsUnit = when (this) {
StatsUnit.HOUR -> StatsVisitsUnit.HOUR
StatsUnit.DAY -> StatsVisitsUnit.DAY
}
}
Loading