diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt index 87a916a7e43..c2716b6e5ca 100644 --- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt +++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt @@ -32,6 +32,7 @@ import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider import com.android.settings.spa.app.specialaccess.SpecialAppAccessPageProvider import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider import com.android.settings.spa.app.specialaccess.UseFullScreenIntentAppListProvider +import com.android.settings.spa.core.instrumentation.SpaLogProvider import com.android.settings.spa.development.UsageStatsPageProvider import com.android.settings.spa.home.HomePageProvider import com.android.settings.spa.network.NetworkAndInternetPageProvider @@ -87,4 +88,5 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) { ), ) } + override val logger = SpaLogProvider } diff --git a/src/com/android/settings/spa/SpaActivity.kt b/src/com/android/settings/spa/SpaActivity.kt index 55883c12264..27f7241b257 100644 --- a/src/com/android/settings/spa/SpaActivity.kt +++ b/src/com/android/settings/spa/SpaActivity.kt @@ -16,18 +16,29 @@ package com.android.settings.spa +import android.app.ActivityManager import android.content.Context import android.content.Intent +import android.os.RemoteException import android.os.UserHandle +import android.util.Log import com.android.settingslib.spa.framework.BrowseActivity +import com.android.settingslib.spa.framework.util.SESSION_BROWSE +import com.android.settingslib.spa.framework.util.SESSION_EXTERNAL import com.android.settingslib.spa.framework.util.appendSpaParams class SpaActivity : BrowseActivity() { companion object { + private const val TAG = "SpaActivity" @JvmStatic fun Context.startSpaActivity(destination: String) { val intent = Intent(this, SpaActivity::class.java) .appendSpaParams(destination = destination) + if (isLaunchedFromInternal()) { + intent.appendSpaParams(sessionName = SESSION_BROWSE) + } else { + intent.appendSpaParams(sessionName = SESSION_EXTERNAL) + } startActivity(intent) } @@ -37,5 +48,15 @@ class SpaActivity : BrowseActivity() { startSpaActivity("$destinationPrefix/$packageName/${UserHandle.myUserId()}") return true } + + fun Context.isLaunchedFromInternal(): Boolean { + var pkg: String? = null + try { + pkg = ActivityManager.getService().getLaunchedFromPackage(getActivityToken()) + } catch (e: RemoteException) { + Log.v(TAG, "Could not talk to activity manager.", e) + } + return applicationContext.packageName == pkg + } } } diff --git a/src/com/android/settings/spa/core/instrumentation/MetricsDataModel.kt b/src/com/android/settings/spa/core/instrumentation/MetricsDataModel.kt new file mode 100644 index 00000000000..62aa8df871a --- /dev/null +++ b/src/com/android/settings/spa/core/instrumentation/MetricsDataModel.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.spa.core.instrumentation + +import androidx.annotation.VisibleForTesting + +/** + * This class stores some metrics temporary data. Such as the timestamp of the page enter for + * calculating the duration time on page. + */ +class MetricsDataModel { + @VisibleForTesting + val pageTimeStampList = mutableListOf() + + fun addTimeStamp(dataItem: PageTimeStamp){ + pageTimeStampList.add(dataItem) + } + + fun getPageDuration(pageId: String, removed: Boolean = true): String { + val lastItem = pageTimeStampList.findLast { it.pageId == pageId } + if (removed && lastItem != null) { + pageTimeStampList.remove(lastItem) + } + return if (lastItem == null) "0" + else (System.currentTimeMillis() - lastItem.timeStamp).toString() + } +} diff --git a/src/com/android/settings/spa/core/instrumentation/SpaLogProvider.kt b/src/com/android/settings/spa/core/instrumentation/SpaLogProvider.kt new file mode 100644 index 00000000000..9b3e2d68ac2 --- /dev/null +++ b/src/com/android/settings/spa/core/instrumentation/SpaLogProvider.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.spa.core.instrumentation + +import android.app.settings.SettingsEnums +import android.os.Bundle +import androidx.annotation.VisibleForTesting +import com.android.settings.core.instrumentation.ElapsedTimeUtils +import com.android.settings.core.instrumentation.SettingsStatsLog +import com.android.settingslib.spa.framework.common.LOG_DATA_SESSION_NAME +import com.android.settingslib.spa.framework.common.LogCategory +import com.android.settingslib.spa.framework.common.LogEvent +import com.android.settingslib.spa.framework.common.SpaLogger +import com.android.settingslib.spa.framework.util.SESSION_BROWSE +import com.android.settingslib.spa.framework.util.SESSION_EXTERNAL +import com.android.settingslib.spa.framework.util.SESSION_SEARCH +import com.android.settingslib.spa.framework.util.SESSION_SLICE +import com.android.settingslib.spa.framework.util.SESSION_UNKNOWN + +/** + * To receive the events from spa framework and logging the these events. + */ +object SpaLogProvider : SpaLogger { + private val dataModel = MetricsDataModel() + + override fun event(id: String, event: LogEvent, category: LogCategory, extraData: Bundle) { + when(event) { + LogEvent.PAGE_ENTER, LogEvent.PAGE_LEAVE -> + write(SpaLogData(id, event, extraData, dataModel)) + else -> return //TODO(b/253979024): Will be implemented in subsequent CLs. + } + } + + private fun write(data: SpaLogData) { + with(data) { + SettingsStatsLog.write( + SettingsStatsLog.SETTINGS_SPA_REPORTED /* atomName */, + getSessionType(), + getPageId(), + getTarget(), + getAction(), + getKey(), + getValue(), + getPreValue(), + getElapsedTime() + ) + } + } +} + +@VisibleForTesting +class SpaLogData(val id: String, val event: LogEvent, + val extraData: Bundle, val dataModel: MetricsDataModel) { + + fun getSessionType(): Int { + if (!extraData.containsKey(LOG_DATA_SESSION_NAME)) { + return SettingsEnums.SESSION_UNKNOWN + } + val sessionSource = extraData.getString(LOG_DATA_SESSION_NAME) + return when(sessionSource) { + SESSION_BROWSE -> SettingsEnums.BROWSE + SESSION_SEARCH -> SettingsEnums.SEARCH + SESSION_SLICE -> SettingsEnums.SLICE_TYPE + SESSION_EXTERNAL -> SettingsEnums.EXTERNAL + else -> SettingsEnums.SESSION_UNKNOWN + } + } + + fun getPageId(): String { + return when(event) { + LogEvent.PAGE_ENTER, LogEvent.PAGE_LEAVE -> id + else -> getPageIdByEntryId(id) + } + } + + //TODO(b/253979024): Will be implemented in subsequent CLs. + fun getTarget(): String? { + return null + } + + fun getAction(): Int { + return event.action + } + + //TODO(b/253979024): Will be implemented in subsequent CLs. + fun getKey(): String? { + return null + } + + fun getValue(): String? { + when(event) { + LogEvent.PAGE_ENTER -> dataModel.addTimeStamp( + PageTimeStamp(id, System.currentTimeMillis())) + LogEvent.PAGE_LEAVE -> return dataModel.getPageDuration(id) + else -> {} //TODO(b/253979024): Will be implemented in subsequent CLs. + } + return null + } + + //TODO(b/253979024): Will be implemented in subsequent CLs. + fun getPreValue(): String? { + return null + } + + fun getElapsedTime(): Long { + return ElapsedTimeUtils.getElapsedTime(System.currentTimeMillis()) + } + + //TODO(b/253979024): Will be implemented in subsequent CLs. + private fun getPageIdByEntryId(id: String): String { + return "" + } +} + +/** + * The buffer is keeping the time stamp while spa page entering. + */ +data class PageTimeStamp(val pageId: String, val timeStamp: Long) diff --git a/tests/spa_unit/src/com/android/settings/spa/SpaActivityTest.kt b/tests/spa_unit/src/com/android/settings/spa/SpaActivityTest.kt index 8a894d5b4a8..46b956e6ccc 100644 --- a/tests/spa_unit/src/com/android/settings/spa/SpaActivityTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/SpaActivityTest.kt @@ -25,12 +25,15 @@ import com.android.settings.spa.SpaActivity.Companion.startSpaActivity import com.android.settings.spa.SpaActivity.Companion.startSpaActivityForApp import com.android.settingslib.spa.framework.util.KEY_DESTINATION import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Answers import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule @@ -39,9 +42,14 @@ class SpaActivityTest { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() - @Mock + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var context: Context + @Before + fun setUp() { + `when`(context.applicationContext.packageName).thenReturn("com.android.settings") + } + @Test fun startSpaActivity() { context.startSpaActivity(DESTINATION) diff --git a/tests/spa_unit/src/com/android/settings/spa/core/instrumentation/MetricsDataModelTest.kt b/tests/spa_unit/src/com/android/settings/spa/core/instrumentation/MetricsDataModelTest.kt new file mode 100644 index 00000000000..2ba03029b5d --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/core/instrumentation/MetricsDataModelTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.spa.core.instrumentation + +import android.os.SystemClock +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for {@link MetricsDataModel}. */ +@RunWith(AndroidJUnit4::class) +class MetricsDataModelTest { + private val TEST_PID = "pseudo_page_id" + + private lateinit var metricsDataModel: MetricsDataModel + + @Before + fun setUp() { + metricsDataModel = MetricsDataModel() + } + + @Test + fun initMetricsDataModel() { + assertThat(metricsDataModel.pageTimeStampList.size).isEqualTo(0) + } + + @Test + fun addTimeStamp_addOnePageTimeStamp_sizeShouldBeOne() { + metricsDataModel.addTimeStamp(PageTimeStamp(TEST_PID, System.currentTimeMillis())) + + assertThat(metricsDataModel.pageTimeStampList.size).isEqualTo(1) + } + + @Test + fun addTimeStamp_addTwoSamePageTimeStamp_sizeShouldBeTwo() { + metricsDataModel.addTimeStamp(PageTimeStamp(TEST_PID, System.currentTimeMillis())) + metricsDataModel.addTimeStamp(PageTimeStamp(TEST_PID, System.currentTimeMillis())) + + assertThat(metricsDataModel.pageTimeStampList.size).isEqualTo(2) + } + + @Test + fun getPageDuration_getExistPageId_mustFoundValue() { + metricsDataModel.addTimeStamp(PageTimeStamp(TEST_PID, System.currentTimeMillis())) + SystemClock.sleep(5) + + assertThat(metricsDataModel.getPageDuration(TEST_PID).toInt()).isGreaterThan(0) + assertThat(metricsDataModel.pageTimeStampList.size).isEqualTo(0) + } + + @Test + fun getPageDuration_getNonExistPageId_valueShouldBeZero() { + metricsDataModel.addTimeStamp(PageTimeStamp(TEST_PID, System.currentTimeMillis())) + + assertThat(metricsDataModel.getPageDuration("WRONG_ID").toLong()).isEqualTo(0L) + } + + @Test + fun getPageDuration_getExistPageIdAndDonotRemoved_sizeShouldBeOne() { + metricsDataModel.addTimeStamp(PageTimeStamp(TEST_PID, System.currentTimeMillis())) + SystemClock.sleep(5) + + assertThat(metricsDataModel.getPageDuration(TEST_PID, false).toLong()).isGreaterThan(0L) + assertThat(metricsDataModel.pageTimeStampList.size).isEqualTo(1) + } + + @Test + fun getPageDuration_getTwoExistPageId_theOrderIsLIFO() { + metricsDataModel.addTimeStamp(PageTimeStamp(TEST_PID, 10000L)) + metricsDataModel.addTimeStamp(PageTimeStamp(TEST_PID, 20000L)) + + // The formula is d1 = t1 - 20000, d2 = t2 - 10000 + // d2 - d1 = t2 - t1 + 10000, because t2 > t1 the result of d2 - d1 is greater 10000 + val duration1 = metricsDataModel.getPageDuration(TEST_PID).toLong() + SystemClock.sleep(5) + val duration2 = metricsDataModel.getPageDuration(TEST_PID).toLong() + + assertThat(duration2 - duration1).isGreaterThan(10000L) + } +} \ No newline at end of file diff --git a/tests/spa_unit/src/com/android/settings/spa/core/instrumentation/SpaLogDataTest.kt b/tests/spa_unit/src/com/android/settings/spa/core/instrumentation/SpaLogDataTest.kt new file mode 100644 index 00000000000..19be10ec892 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/core/instrumentation/SpaLogDataTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.spa.core.instrumentation + +import android.app.settings.SettingsEnums +import android.os.Bundle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.framework.common.LOG_DATA_SESSION_NAME +import com.android.settingslib.spa.framework.common.LogEvent +import com.android.settingslib.spa.framework.util.SESSION_BROWSE +import com.android.settingslib.spa.framework.util.SESSION_SEARCH +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for {@link SpaLogData}. */ +@RunWith(AndroidJUnit4::class) +class SpaLogDataTest { + private val TEST_PID = "pseudo_page_id" + + private lateinit var bundle: Bundle + private lateinit var dataModel: MetricsDataModel + + @Before + fun setUp() { + bundle = Bundle() + dataModel = MetricsDataModel() + } + + @Test + fun getSessionType_withoutSessionExtraData_returnSessionUnknow() { + val spaLogData = SpaLogData(TEST_PID, LogEvent.PAGE_ENTER, bundle, dataModel) + + assertThat(spaLogData.getSessionType()).isEqualTo(SettingsEnums.SESSION_UNKNOWN) + } + + @Test + fun getSessionType_hasSessionBrowseExtraData_returnSessionBrowse() { + bundle.putString(LOG_DATA_SESSION_NAME, SESSION_BROWSE) + val spaLogData = SpaLogData(TEST_PID, LogEvent.PAGE_ENTER, bundle, dataModel) + + assertThat(spaLogData.getSessionType()).isEqualTo(SettingsEnums.BROWSE) + } + + @Test + fun getSessionType_hasSessionSearchExtraData_returnSessionSearch() { + bundle.putString(LOG_DATA_SESSION_NAME, SESSION_SEARCH) + val spaLogData = SpaLogData(TEST_PID, LogEvent.PAGE_ENTER, bundle, dataModel) + + assertThat(spaLogData.getSessionType()).isEqualTo(SettingsEnums.SEARCH) + } + + @Test + fun getSessionType_hasSessionUnknownExtraData_returnSessionUnknow() { + bundle.putString(LOG_DATA_SESSION_NAME, "SESSION_OTHER") + val spaLogData = SpaLogData(TEST_PID, LogEvent.PAGE_ENTER, bundle, dataModel) + + assertThat(spaLogData.getSessionType()).isEqualTo(SettingsEnums.SESSION_UNKNOWN) + } + + @Test + fun getPageId_withPageEvent_returnInputId() { + val spaLogData1 = SpaLogData(TEST_PID, LogEvent.PAGE_ENTER, bundle, dataModel) + assertThat(spaLogData1.getPageId()).isEqualTo(TEST_PID) + + val spaLogData2 = SpaLogData(TEST_PID, LogEvent.PAGE_LEAVE, bundle, dataModel) + assertThat(spaLogData2.getPageId()).isEqualTo(TEST_PID) + } +}