Skip to content

Commit c685c68

Browse files
authored
Duck.ai: Tabs lifecycle (#7131)
Task/Issue URL: https://app.asana.com/1/137249556945/task/1211821284193230 ### Description This PR ensures that opening links in Duck.ai mode are stored as Tabs and can be shown as such in the Tab Manager ### Steps to test this PR Enable Fullscreen mode in AI Features _Input Screen disabled - New Tab_ - [x] Fresh install, ensure Duck.ai is enabled - [x] Open New Tab - [x] Open Duck.ai from the Omnibar (Duck icon) - [x] Verify Duck.ai loads in the same screen - [x] Open Tab Manager - [x] Verify tab is displayed as Duck.ai and there’s a preview image of the chat _Input Screen disabled - Browser_ - [x] Fresh install, ensure Duck.ai is enabled - [x] Open New Tab and navigate to any website - [x] Open Duck.ai from the Omnibar (Duck icon) - [x] Verify Duck.ai loads in the same screen - [x] Open Tab Manager - [x] Verify tab is displayed as Duck.ai and there’s a preview image of the chat _Browser Menu - New Tab_ - [x] Fresh install, ensure Duck.ai is enabled - [x] Open New Tab - [x] Open Browser Menu and tap on Duck.ai - [x] Verify Duck.ai loads in the same screen - [x] Open Tab Manager - [x] Verify tab is displayed as Duck.ai and there’s a preview image of the chat _Browser Menu - Browser_ - [x] Fresh install, ensure Duck.ai is enabled - [x] Open New Tab and navigate to any website - [x] Open Browser Menu and tap on Duck.ai - [x] Verify Duck.ai loads in the same screen - [x] Open Tab Manager - [x] Verify tab is displayed as Duck.ai and there’s a preview image of the chat _Input Screen enabled - New Tab_ - [x] Fresh install, ensure Duck.ai is enabled and also Search & Duck.ai mode - [x] Open New Tab - [x] Enter a query from the Input Screen in Duck.ai mode - [x] Verify Duck.ai loads - [x] Open Tab Manager - [x] Verify tab is displayed as Duck.ai and there’s a preview image of the chat _Input Screen enabled - Browser_ - [x] Fresh install, ensure Duck.ai is enabled - [x] Open New Tab and navigate to any website - [x] Enter a query from the Input Screen in Duck.ai mode - [x] Verify Duck.ai loads - [x] Open Tab Manager - [x] Verify tab is displayed as Duck.ai and there’s a preview image of the chat
1 parent a60a957 commit c685c68

File tree

16 files changed

+221
-53
lines changed

16 files changed

+221
-53
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,8 @@ class BrowserTabViewModelTest {
459459

460460
private val mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow = MutableStateFlow(false)
461461

462+
private val mockDuckAiFeatureStateFullScreenModeFlow = MutableStateFlow(false)
463+
462464
private val mockExternalIntentProcessingState: ExternalIntentProcessingState = mock()
463465

464466
private val mockVpnMenuStateProvider: VpnMenuStateProvider = mock()
@@ -607,6 +609,7 @@ class BrowserTabViewModelTest {
607609

608610
private val exampleUrl = "http://example.com"
609611
private val shortExampleUrl = "example.com"
612+
private val duckChatURL = "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5"
610613

611614
private val selectedTab = TabEntity("TAB_ID", exampleUrl, position = 0, sourceTabId = "TAB_ID_SOURCE")
612615
private val flowSelectedTab = MutableStateFlow(selectedTab)
@@ -620,6 +623,7 @@ class BrowserTabViewModelTest {
620623
private val mockDeviceAppLookup: DeviceAppLookup = mock()
621624

622625
private val mockDuckAiFullScreenMode = MutableStateFlow(false)
626+
private val mockDuckAiFullScreenModeEnabled = MutableStateFlow(false)
623627

624628
private lateinit var fakeContentScopeScriptsSubscriptionEventPluginPoint: FakeContentScopeScriptsSubscriptionEventPluginPoint
625629
private var fakeSettingsPageFeature = FakeFeatureToggleFactory.create(SettingsPageFeature::class.java)
@@ -692,6 +696,7 @@ class BrowserTabViewModelTest {
692696
whenever(mockDuckAiFeatureState.showPopupMenuShortcut).thenReturn(MutableStateFlow(false))
693697
whenever(mockDuckAiFeatureState.showInputScreen).thenReturn(mockDuckAiFeatureStateInputScreenFlow)
694698
whenever(mockDuckAiFeatureState.showInputScreenAutomaticallyOnNewTab).thenReturn(mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow)
699+
whenever(mockDuckAiFeatureState.showFullScreenMode).thenReturn(mockDuckAiFeatureStateFullScreenModeFlow)
695700
whenever(mockExternalIntentProcessingState.hasPendingTabLaunch).thenReturn(mockHasPendingTabLaunchFlow)
696701
whenever(mockExternalIntentProcessingState.hasPendingDuckAiOpen).thenReturn(mockHasPendingDuckAiOpenFlow)
697702
whenever(mockVpnMenuStateProvider.getVpnMenuState()).thenReturn(flowOf(VpnMenuState.Hidden))
@@ -765,6 +770,8 @@ class BrowserTabViewModelTest {
765770

766771
fakeContentScopeScriptsSubscriptionEventPluginPoint = FakeContentScopeScriptsSubscriptionEventPluginPoint()
767772

773+
whenever(mockDuckChat.getDuckChatUrl(any(), any())).thenReturn(duckChatURL)
774+
768775
testee =
769776
BrowserTabViewModel(
770777
statisticsUpdater = mockStatisticsUpdater,
@@ -6341,6 +6348,22 @@ class BrowserTabViewModelTest {
63416348
verify(mockDuckChat).openDuckChat()
63426349
}
63436350

6351+
@Test
6352+
fun whenDuckChatMenuItemClickedAndFullScreenModeThenDontOpenDuckChatScreen() =
6353+
runTest {
6354+
mockDuckAiFeatureStateFullScreenModeFlow.emit(true)
6355+
whenever(mockDuckChat.wasOpenedBefore()).thenReturn(false)
6356+
whenever(mockOmnibarConverter.convertQueryToUrl(duckChatURL, null)).thenReturn(duckChatURL)
6357+
6358+
testee.onDuckChatMenuClicked()
6359+
6360+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
6361+
6362+
assertTrue(commandCaptor.lastValue is Navigate)
6363+
6364+
verify(mockDuckChat, never()).openDuckChat()
6365+
}
6366+
63446367
@Test
63456368
fun whenDuckChatMenuItemClickedAndItWasUsedBeforeThenOpenDuckChatAndSendPixel() =
63466369
runTest {
@@ -6400,6 +6423,22 @@ class BrowserTabViewModelTest {
64006423
verify(mockDuckChat).openDuckChat()
64016424
}
64026425

6426+
@Test
6427+
fun whenOnDuckChatOmnibarButtonClickedAndFullScreenModeThenOpenChatNotCalled() = runTest {
6428+
val duckAIUrl = "https://duckduckgo.com/?q=test"
6429+
mockDuckAiFeatureStateFullScreenModeFlow.emit(true)
6430+
whenever(mockDuckChat.getDuckChatUrl(any(), any())).thenReturn(duckAIUrl)
6431+
whenever(mockOmnibarConverter.convertQueryToUrl(duckAIUrl, null)).thenReturn(duckAIUrl)
6432+
6433+
testee.onDuckChatOmnibarButtonClicked(query = "example", hasFocus = false, isNtp = true)
6434+
6435+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
6436+
val command = commandCaptor.lastValue as Navigate
6437+
assertEquals(duckAIUrl, command.url)
6438+
6439+
verify(mockDuckChat, never()).openDuckChat()
6440+
}
6441+
64036442
@Test
64046443
fun whenPageFinishedWithMaliciousSiteBlockedThenDoNotUpdateSite() {
64056444
testee.browserViewState.value =

app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,6 @@ open class BrowserActivity : DuckDuckGoActivity() {
743743
command.duckChatSessionActive,
744744
command.withTransition,
745745
command.tabs,
746-
command.fullScreenMode,
747746
)
748747

749748
Command.LaunchTabSwitcher -> currentTab?.launchTabSwitcherAfterTabsUndeleted()
@@ -837,24 +836,19 @@ open class BrowserActivity : DuckDuckGoActivity() {
837836
duckChatSessionActive: Boolean,
838837
withTransition: Boolean,
839838
tabs: Int,
840-
fullScreenMode: Boolean,
841839
) {
842-
if (fullScreenMode) {
843-
currentTab?.submitQuery(url!!)
844-
} else {
845-
duckAiFragment?.let { fragment ->
846-
if (duckChatSessionActive) {
847-
restoreDuckChat(fragment, withTransition)
848-
} else {
849-
launchNewDuckChat(url, withTransition, tabs)
850-
}
851-
} ?: run {
840+
duckAiFragment?.let { fragment ->
841+
if (duckChatSessionActive) {
842+
restoreDuckChat(fragment, withTransition)
843+
} else {
852844
launchNewDuckChat(url, withTransition, tabs)
853845
}
846+
} ?: run {
847+
launchNewDuckChat(url, withTransition, tabs)
848+
}
854849

855-
currentTab?.getOmnibar()?.omnibarView?.omnibarTextInput?.let {
856-
hideKeyboard(it)
857-
}
850+
currentTab?.getOmnibar()?.omnibarView?.omnibarTextInput?.let {
851+
hideKeyboard(it)
858852
}
859853
}
860854

app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,7 @@ class BrowserTabViewModel @Inject constructor(
10791079
query: String,
10801080
queryOrigin: QueryOrigin = QueryOrigin.FromUser,
10811081
) {
1082+
logcat { "Duck.ai: onUserSubmittedQuery $query" }
10821083
navigationAwareLoginDetector.onEvent(NavigationEvent.UserAction.NewQuerySubmitted)
10831084

10841085
if (query.isBlank()) {
@@ -1132,19 +1133,17 @@ class BrowserTabViewModel @Inject constructor(
11321133
val verticalParameter = extractVerticalParameter(url)
11331134
var urlToNavigate = queryUrlConverter.convertQueryToUrl(trimmedInput, verticalParameter, queryOrigin)
11341135

1136+
logcat { "Duck.ai: urlToNavigate $urlToNavigate" }
1137+
11351138
when (val type = specialUrlDetector.determineType(trimmedInput)) {
11361139
is ShouldLaunchDuckChatLink -> {
11371140
runCatching {
1138-
if (duckAiFeatureState.showFullScreenMode.value) {
1139-
site?.nextUrl = urlToNavigate
1140-
command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate))
1141+
logcat { "Duck.ai: ShouldLaunchDuckChatLink $urlToNavigate" }
1142+
val queryParameter = urlToNavigate.toUri().getQueryParameter(QUERY)
1143+
if (queryParameter != null) {
1144+
duckChat.openDuckChatWithPrefill(queryParameter)
11411145
} else {
1142-
val queryParameter = urlToNavigate.toUri().getQueryParameter(QUERY)
1143-
if (queryParameter != null) {
1144-
duckChat.openDuckChatWithPrefill(queryParameter)
1145-
} else {
1146-
duckChat.openDuckChat()
1147-
}
1146+
duckChat.openDuckChat()
11481147
}
11491148
return
11501149
}
@@ -1544,6 +1543,7 @@ class BrowserTabViewModel @Inject constructor(
15441543

15451544
when (stateChange) {
15461545
is WebNavigationStateChange.NewPage -> {
1546+
logcat { "Duck.ai: WebNavigationStateChange.NewPage ${stateChange.url.toUri()}" }
15471547
val uri = stateChange.url.toUri()
15481548
viewModelScope.launch(dispatchers.io()) {
15491549
if (duckPlayer.getDuckPlayerState() == ENABLED && duckPlayer.isSimulatedYoutubeNoCookie(uri)) {
@@ -1569,6 +1569,7 @@ class BrowserTabViewModel @Inject constructor(
15691569

15701570
is WebNavigationStateChange.PageCleared -> pageCleared()
15711571
is WebNavigationStateChange.UrlUpdated -> {
1572+
logcat { "Duck.ai: urlUpdated ${stateChange.url}" }
15721573
val uri = stateChange.url.toUri()
15731574
viewModelScope.launch(dispatchers.io()) {
15741575
if (duckPlayer.getDuckPlayerState() == ENABLED && duckPlayer.isSimulatedYoutubeNoCookie(uri)) {
@@ -1626,7 +1627,7 @@ class BrowserTabViewModel @Inject constructor(
16261627
url: String,
16271628
title: String?,
16281629
) {
1629-
logcat(VERBOSE) { "Page changed: $url" }
1630+
logcat(VERBOSE) { "Duck.ai: Page changed: $url" }
16301631
cleanupBlobDownloadReplyProxyMaps()
16311632

16321633
hasCtaBeenShownForCurrentPage.set(false)
@@ -1899,6 +1900,7 @@ class BrowserTabViewModel @Inject constructor(
18991900
}
19001901

19011902
override fun pageRefreshed(refreshedUrl: String) {
1903+
logcat { "Duck.ai pageRefreshed URL: $url refreshedUrl $refreshedUrl" }
19021904
if (url == null || refreshedUrl == url) {
19031905
logcat(VERBOSE) { "Page refreshed: $refreshedUrl" }
19041906
pageChanged(refreshedUrl, title)
@@ -2185,6 +2187,7 @@ class BrowserTabViewModel @Inject constructor(
21852187
}
21862188

21872189
private fun onSiteChanged() {
2190+
logcat { "Duck.ai: onSiteChanged" }
21882191
httpsUpgraded = false
21892192
site?.isDesktopMode = currentBrowserViewState().isDesktopBrowsingMode
21902193
viewModelScope.launch {
@@ -4470,7 +4473,13 @@ class BrowserTabViewModel @Inject constructor(
44704473
val params = duckChat.createWasUsedBeforePixelParams()
44714474
pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN_BROWSER_MENU, parameters = params)
44724475
}
4473-
duckChat.openDuckChat()
4476+
4477+
if (duckAiFeatureState.showFullScreenMode.value) {
4478+
val url = duckChat.getDuckChatUrl("", false)
4479+
onUserSubmittedQuery(url)
4480+
} else {
4481+
duckChat.openDuckChat()
4482+
}
44744483
}
44754484

44764485
fun onDuckChatOmnibarButtonClicked(
@@ -4481,10 +4490,20 @@ class BrowserTabViewModel @Inject constructor(
44814490
viewModelScope.launch {
44824491
command.value = HideKeyboardForChat
44834492
}
4484-
when {
4485-
hasFocus && isNtp && query.isNullOrBlank() -> duckChat.openDuckChat()
4486-
hasFocus -> duckChat.openDuckChatWithAutoPrompt(query ?: "")
4487-
else -> duckChat.openDuckChat()
4493+
4494+
if (duckAiFeatureState.showFullScreenMode.value) {
4495+
val url = when {
4496+
hasFocus && isNtp && query.isNullOrBlank() -> duckChat.getDuckChatUrl(query ?: "", false)
4497+
hasFocus -> duckChat.getDuckChatUrl(query ?: "", true)
4498+
else -> duckChat.getDuckChatUrl(query ?: "", false)
4499+
}
4500+
onUserSubmittedQuery(url)
4501+
} else {
4502+
when {
4503+
hasFocus && isNtp && query.isNullOrBlank() -> duckChat.openDuckChat()
4504+
hasFocus -> duckChat.openDuckChatWithAutoPrompt(query ?: "")
4505+
else -> duckChat.openDuckChat()
4506+
}
44884507
}
44894508
}
44904509

app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ class BrowserViewModel @Inject constructor(
144144
val duckChatSessionActive: Boolean,
145145
val withTransition: Boolean,
146146
val tabs: Int,
147-
val fullScreenMode: Boolean,
148147
) : Command()
149148
}
150149

@@ -507,10 +506,9 @@ class BrowserViewModel @Inject constructor(
507506
duckChatSessionActive: Boolean,
508507
withTransition: Boolean,
509508
) {
510-
val duckAiFullScreenMode = duckAiFeatureState.showFullScreenMode.value
511509
logcat(INFO) { "Duck.ai openDuckChat duckChatSessionActive $duckChatSessionActive" }
512510
val tabsCount = tabs.value?.size ?: 0
513-
sendCommand(OpenDuckChat(duckChatUrl, duckChatSessionActive, withTransition, tabsCount, duckAiFullScreenMode))
511+
sendCommand(OpenDuckChat(duckChatUrl, duckChatSessionActive, withTransition, tabsCount))
514512
}
515513
}
516514

app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ class BrowserWebViewClient @Inject constructor(
470470
url: String?,
471471
favicon: Bitmap?,
472472
) {
473+
logcat { "Duck.ai onPageStarted webViewUrl: ${webView.url} URL: $url lastPageStarted $lastPageStarted" }
473474
url?.let {
474475
// See https://app.asana.com/0/0/1206159443951489/f (WebView limitations)
475476
if (it != ABOUT_BLANK && start == null) {

app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType
3030
import com.duckduckgo.app.browser.applinks.ExternalAppIntentFlagsFeature
3131
import com.duckduckgo.app.browser.duckchat.AIChatQueryDetectionFeature
3232
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
33+
import com.duckduckgo.duckchat.api.DuckAiFeatureState
3334
import com.duckduckgo.duckchat.api.DuckChat
3435
import com.duckduckgo.duckplayer.api.DuckPlayer
3536
import com.duckduckgo.privacy.config.api.AmpLinkType
@@ -49,11 +50,15 @@ class SpecialUrlDetectorImpl(
4950
private val externalAppIntentFlagsFeature: ExternalAppIntentFlagsFeature,
5051
private val duckPlayer: DuckPlayer,
5152
private val duckChat: DuckChat,
53+
private val duckAiFeatureState: DuckAiFeatureState,
5254
private val aiChatQueryDetectionFeature: AIChatQueryDetectionFeature,
5355
private val androidBrowserConfigFeature: AndroidBrowserConfigFeature,
5456
) : SpecialUrlDetector {
5557

56-
override fun determineType(initiatingUrl: String?, uri: Uri): UrlType {
58+
override fun determineType(
59+
initiatingUrl: String?,
60+
uri: Uri,
61+
): UrlType {
5762
val uriString = uri.toString()
5863

5964
return when (val scheme = uri.scheme) {
@@ -69,12 +74,15 @@ class SpecialUrlDetectorImpl(
6974
null -> {
7075
if (subscriptions.shouldLaunchPrivacyProForUrl("https://$uriString")) {
7176
UrlType.ShouldLaunchPrivacyProLink
72-
} else if (aiChatQueryDetectionFeature.self().isEnabled() && duckChat.isDuckChatUrl(uri)) {
77+
} else if (aiChatQueryDetectionFeature.self()
78+
.isEnabled() && duckChat.isDuckChatUrl(uri) && !duckAiFeatureState.showFullScreenMode.value
79+
) {
7380
UrlType.ShouldLaunchDuckChatLink
7481
} else {
7582
UrlType.SearchQuery(uriString)
7683
}
7784
}
85+
7886
else -> {
7987
val intentFlags = if (scheme == INTENT_SCHEME && androidBrowserConfigFeature.handleIntentScheme().isEnabled()) {
8088
URI_INTENT_SCHEME
@@ -100,14 +108,17 @@ class SpecialUrlDetectorImpl(
100108
private fun buildSmsTo(uriString: String): UrlType = UrlType.Sms(uriString.removePrefix("$SMSTO_SCHEME:").truncate(SMS_MAX_LENGTH))
101109

102110
@Suppress("NewApi") // we use appBuildConfig
103-
override fun processUrl(initiatingUrl: String?, uriString: String): UrlType {
111+
override fun processUrl(
112+
initiatingUrl: String?,
113+
uriString: String,
114+
): UrlType {
104115
trackingParameters.cleanTrackingParameters(initiatingUrl = initiatingUrl, url = uriString)?.let { cleanedUrl ->
105116
return UrlType.TrackingParameterLink(cleanedUrl = cleanedUrl)
106117
}
107118

108119
val uri = uriString.toUri()
109120

110-
if (duckChat.isDuckChatUrl(uri)) {
121+
if (duckChat.isDuckChatUrl(uri) && !duckAiFeatureState.showFullScreenMode.value) {
111122
return UrlType.ShouldLaunchDuckChatLink
112123
}
113124

@@ -187,7 +198,11 @@ class SpecialUrlDetectorImpl(
187198
intentFlags: Int,
188199
userInitiated: Boolean,
189200
): UrlType {
190-
fun buildIntent(uriString: String, intentFlags: Int, userInitiated: Boolean): UrlType {
201+
fun buildIntent(
202+
uriString: String,
203+
intentFlags: Int,
204+
userInitiated: Boolean,
205+
): UrlType {
191206
return try {
192207
val intent = Intent.parseUri(uriString, intentFlags)
193208
// only proceed if something can handle it

app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import com.duckduckgo.downloads.api.FileDownloader
8484
import com.duckduckgo.downloads.impl.AndroidFileDownloader
8585
import com.duckduckgo.downloads.impl.DataUriDownloader
8686
import com.duckduckgo.downloads.impl.FileDownloadCallback
87+
import com.duckduckgo.duckchat.api.DuckAiFeatureState
8788
import com.duckduckgo.duckchat.api.DuckChat
8889
import com.duckduckgo.duckplayer.api.DuckPlayer
8990
import com.duckduckgo.experiments.api.VariantManager
@@ -193,6 +194,7 @@ class BrowserModule {
193194
externalAppIntentFlagsFeature: ExternalAppIntentFlagsFeature,
194195
duckPlayer: DuckPlayer,
195196
duckChat: DuckChat,
197+
duckChaFeatureState: DuckAiFeatureState,
196198
aiChatQueryDetectionFeature: AIChatQueryDetectionFeature,
197199
androidBrowserConfigFeature: AndroidBrowserConfigFeature,
198200
): SpecialUrlDetector = SpecialUrlDetectorImpl(
@@ -203,6 +205,7 @@ class BrowserModule {
203205
externalAppIntentFlagsFeature,
204206
duckPlayer,
205207
duckChat,
208+
duckChaFeatureState,
206209
aiChatQueryDetectionFeature,
207210
androidBrowserConfigFeature,
208211
)

app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ class TabDataRepository @Inject constructor(
287287
site: Site?,
288288
) {
289289
databaseExecutor().scheduleDirect {
290+
logcat { "Duck.ai: updateUrlAndTitle url: ${site?.url} title: ${site?.title}" }
290291
tabsDao.updateUrlAndTitle(tabId, site?.url, site?.title, viewed = true)
291292
}
292293
}

0 commit comments

Comments
 (0)