Skip to content

Commit 10d743d

Browse files
committed
Merge branch 'trunk' into feature/tags-ia
2 parents 13c2b12 + 1292631 commit 10d743d

File tree

104 files changed

+3208
-2769
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+3208
-2769
lines changed
Binary file not shown.

WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ sealed class MainActionListItem {
1313
CREATE_NEW_PAGE,
1414
CREATE_NEW_PAGE_FROM_PAGES_CARD,
1515
CREATE_NEW_POST,
16-
ANSWER_BLOGGING_PROMPT
16+
ANSWER_BLOGGING_PROMPT,
17+
CREATE_NEW_POST_FROM_AUDIO_AI
1718
}
1819

1920
data class CreateAction(

WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@
148148
import org.wordpress.android.ui.uploads.UploadUtilsWrapper;
149149
import org.wordpress.android.ui.utils.JetpackAppMigrationFlowUtils;
150150
import org.wordpress.android.ui.utils.UiString.UiStringRes;
151+
import org.wordpress.android.ui.voicetocontent.VoiceToContentDialogFragment;
151152
import org.wordpress.android.ui.whatsnew.FeatureAnnouncementDialogFragment;
152153
import org.wordpress.android.util.AniUtils;
153154
import org.wordpress.android.util.AppLog;
@@ -719,6 +720,9 @@ private void initViewModel() {
719720

720721
mViewModel.getCreateAction().observe(this, createAction -> {
721722
switch (createAction) {
723+
case CREATE_NEW_POST_FROM_AUDIO_AI:
724+
launchVoiceToContent();
725+
break;
722726
case CREATE_NEW_POST:
723727
handleNewPostAction(PagePostCreationSourcesDetail.POST_FROM_MY_SITE, -1, null);
724728
break;
@@ -1325,6 +1329,15 @@ private void handleNewPostAction(PagePostCreationSourcesDetail source,
13251329
ActivityLauncher.addNewPostForResult(this, getSelectedSite(), false, source, promptId, entryPoint);
13261330
}
13271331

1332+
private void launchVoiceToContent() {
1333+
if (!mSiteStore.hasSite()) {
1334+
// No site yet - Move to My Sites fragment that shows the create new site screen
1335+
mBottomNav.setCurrentSelectedPage(PageType.MY_SITE);
1336+
return;
1337+
}
1338+
VoiceToContentDialogFragment.newInstance().show(getSupportFragmentManager(), VoiceToContentDialogFragment.TAG);
1339+
}
1340+
13281341
private void trackLastVisiblePage(@NonNull final PageType pageType) {
13291342
switch (pageType) {
13301343
case MY_SITE:
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package org.wordpress.android.ui.voicetocontent
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.compose.foundation.clickable
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.runtime.getValue
10+
import androidx.compose.runtime.livedata.observeAsState
11+
import androidx.compose.ui.platform.ComposeView
12+
import androidx.fragment.app.viewModels
13+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
14+
import dagger.hilt.android.AndroidEntryPoint
15+
import org.wordpress.android.ui.compose.theme.AppTheme
16+
import androidx.compose.foundation.layout.Column
17+
import androidx.compose.foundation.layout.Spacer
18+
import androidx.compose.foundation.layout.fillMaxWidth
19+
import androidx.compose.foundation.layout.height
20+
import androidx.compose.foundation.layout.padding
21+
import androidx.compose.foundation.layout.size
22+
import androidx.compose.material.Icon
23+
import androidx.compose.material.Text
24+
import androidx.compose.ui.Alignment
25+
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.res.painterResource
27+
import androidx.compose.ui.text.font.FontWeight
28+
import androidx.compose.ui.unit.dp
29+
import androidx.compose.ui.unit.sp
30+
import org.wordpress.android.R
31+
32+
@AndroidEntryPoint
33+
class VoiceToContentDialogFragment : BottomSheetDialogFragment() {
34+
private val viewModel: VoiceToContentViewModel by viewModels()
35+
36+
override fun onCreateView(
37+
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
38+
): View = ComposeView(requireContext()).apply {
39+
setContent {
40+
AppTheme {
41+
VoiceToContentScreen(viewModel)
42+
}
43+
}
44+
}
45+
46+
companion object {
47+
const val TAG = "voice_to_content_fragment_tag"
48+
49+
@JvmStatic
50+
fun newInstance() = VoiceToContentDialogFragment()
51+
}
52+
}
53+
54+
@Composable
55+
fun VoiceToContentScreen(viewModel: VoiceToContentViewModel) {
56+
val result by viewModel.uiState.observeAsState()
57+
Column(
58+
horizontalAlignment = Alignment.CenterHorizontally,
59+
modifier = Modifier
60+
.fillMaxWidth()
61+
.padding(16.dp)
62+
) {
63+
when {
64+
result?.isError == true -> {
65+
Text(text = "Error happened", fontSize = 20.sp, fontWeight = FontWeight.Bold)
66+
}
67+
68+
result?.content != null -> {
69+
Text(text = result?.content!!, fontSize = 20.sp, fontWeight = FontWeight.Bold)
70+
}
71+
72+
else -> {
73+
Text(text = "Ready to fake record - tap microphone", fontSize = 20.sp, fontWeight = FontWeight.Bold)
74+
Spacer(modifier = Modifier.height(16.dp))
75+
Icon(
76+
painterResource(id = R.drawable.ic_mic_white_24dp),
77+
contentDescription = "Microphone",
78+
modifier = Modifier
79+
.size(64.dp)
80+
.clickable { viewModel.execute() }
81+
)
82+
}
83+
}
84+
}
85+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.wordpress.android.ui.voicetocontent
2+
3+
import org.wordpress.android.util.BuildConfigWrapper
4+
import org.wordpress.android.util.config.VoiceToContentFeatureConfig
5+
import javax.inject.Inject
6+
7+
class VoiceToContentFeatureUtils @Inject constructor(
8+
private val buildConfigWrapper: BuildConfigWrapper,
9+
private val voiceToContentFeatureConfig: VoiceToContentFeatureConfig
10+
) {
11+
fun isVoiceToContentEnabled() = buildConfigWrapper.isJetpackApp && voiceToContentFeatureConfig.isEnabled()
12+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.wordpress.android.ui.voicetocontent
2+
3+
import android.content.Context
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.withContext
6+
import org.wordpress.android.fluxc.model.SiteModel
7+
import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAITranscriptionRestClient
8+
import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore
9+
import org.wordpress.android.viewmodel.ContextProvider
10+
import java.io.File
11+
import java.io.FileOutputStream
12+
import java.io.InputStream
13+
import javax.inject.Inject
14+
15+
class VoiceToContentUseCase @Inject constructor(
16+
private val jetpackAIStore: JetpackAIStore,
17+
private val contextProvider: ContextProvider
18+
) {
19+
companion object {
20+
const val FEATURE = "voice_to_content"
21+
private const val KILO_BYTE = 1024
22+
}
23+
24+
suspend fun execute(
25+
siteModel: SiteModel,
26+
): VoiceToContentResult =
27+
withContext(Dispatchers.IO) {
28+
val file = getAudioFile() ?: return@withContext VoiceToContentResult(isError = true)
29+
val response = jetpackAIStore.fetchJetpackAITranscription(
30+
siteModel,
31+
FEATURE,
32+
file
33+
)
34+
35+
when(response) {
36+
is JetpackAITranscriptionRestClient.JetpackAITranscriptionResponse.Success -> {
37+
return@withContext VoiceToContentResult(content = response.model)
38+
}
39+
is JetpackAITranscriptionRestClient.JetpackAITranscriptionResponse.Error -> {
40+
return@withContext VoiceToContentResult(isError = true)
41+
}
42+
}
43+
}
44+
45+
// todo: The next three methods are temporary to support development - remove when the real impl is in place
46+
private fun getAudioFile(): File? {
47+
val result = runCatching {
48+
getFileFromAssets(contextProvider.getContext())
49+
}
50+
51+
return result.getOrElse {
52+
null
53+
}
54+
}
55+
56+
// todo: Do not forget to delete the test file from the asset directory - when the real impl is in place
57+
private fun getFileFromAssets(context: Context): File {
58+
val fileName = "jetpack-ai-transcription-test-audio-file.m4a"
59+
val file = File(context.filesDir, fileName)
60+
context.assets.open(fileName).use { inputStream ->
61+
copyInputStreamToFile(inputStream, file)
62+
}
63+
return file
64+
}
65+
66+
private fun copyInputStreamToFile(inputStream: InputStream, outputFile: File) {
67+
FileOutputStream(outputFile).use { outputStream ->
68+
val buffer = ByteArray(KILO_BYTE)
69+
var length: Int
70+
while (inputStream.read(buffer).also { length = it } > 0) {
71+
outputStream.write(buffer, 0, length)
72+
}
73+
outputStream.flush()
74+
}
75+
inputStream.close()
76+
}
77+
}
78+
79+
// todo: build out the result object
80+
data class VoiceToContentResult(
81+
val content: String? = null,
82+
val isError: Boolean = false
83+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.wordpress.android.ui.voicetocontent
2+
3+
import androidx.lifecycle.LiveData
4+
import androidx.lifecycle.MutableLiveData
5+
import androidx.lifecycle.viewModelScope
6+
import dagger.hilt.android.lifecycle.HiltViewModel
7+
import kotlinx.coroutines.CoroutineDispatcher
8+
import kotlinx.coroutines.launch
9+
import org.wordpress.android.modules.UI_THREAD
10+
import org.wordpress.android.ui.mysite.SelectedSiteRepository
11+
import org.wordpress.android.viewmodel.ScopedViewModel
12+
import javax.inject.Inject
13+
import javax.inject.Named
14+
15+
@HiltViewModel
16+
class VoiceToContentViewModel @Inject constructor(
17+
@Named(UI_THREAD) mainDispatcher: CoroutineDispatcher,
18+
private val voiceToContentFeatureUtils: VoiceToContentFeatureUtils,
19+
private val voiceToContentUseCase: VoiceToContentUseCase,
20+
private val selectedSiteRepository: SelectedSiteRepository
21+
) : ScopedViewModel(mainDispatcher) {
22+
private val _uiState = MutableLiveData<VoiceToContentResult>()
23+
val uiState = _uiState as LiveData<VoiceToContentResult>
24+
25+
private fun isVoiceToContentEnabled() = voiceToContentFeatureUtils.isVoiceToContentEnabled()
26+
27+
fun execute() {
28+
val site = selectedSiteRepository.getSelectedSite() ?: run {
29+
_uiState.postValue(VoiceToContentResult(isError = true))
30+
return
31+
}
32+
33+
if (isVoiceToContentEnabled()) {
34+
viewModelScope.launch {
35+
val result = voiceToContentUseCase.execute(site)
36+
_uiState.postValue(result)
37+
}
38+
}
39+
}
40+
}

WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository
3636
import org.wordpress.android.ui.prefs.AppPrefsWrapper
3737
import org.wordpress.android.ui.prefs.privacy.banner.domain.ShouldAskPrivacyConsent
3838
import org.wordpress.android.ui.utils.UiString.UiStringText
39+
import org.wordpress.android.ui.voicetocontent.VoiceToContentFeatureUtils
3940
import org.wordpress.android.ui.whatsnew.FeatureAnnouncementProvider
4041
import org.wordpress.android.util.BuildConfigWrapper
4142
import org.wordpress.android.util.FluxCUtils
@@ -68,6 +69,7 @@ class WPMainActivityViewModel @Inject constructor(
6869
private val bloggingPromptsStore: BloggingPromptsStore,
6970
@Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher,
7071
private val shouldAskPrivacyConsent: ShouldAskPrivacyConsent,
72+
private val voiceToContentFeatureUtils: VoiceToContentFeatureUtils
7173
) : ScopedViewModel(mainDispatcher) {
7274
private var isStarted = false
7375

@@ -203,6 +205,16 @@ class WPMainActivityViewModel @Inject constructor(
203205
onClickAction = ::onCreateActionClicked
204206
)
205207
)
208+
if (voiceToContentFeatureUtils.isVoiceToContentEnabled() && hasFullAccessToContent(site)) {
209+
actionsList.add(
210+
CreateAction(
211+
actionType = ActionType.CREATE_NEW_POST_FROM_AUDIO_AI,
212+
iconRes = R.drawable.ic_mic_white_24dp,
213+
labelRes = R.string.my_site_bottom_sheet_add_post_from_audio,
214+
onClickAction = ::onCreateActionClicked
215+
)
216+
)
217+
}
206218
if (hasFullAccessToContent(site)) {
207219
actionsList.add(
208220
CreateAction(
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
2+
3+
<path android:fillColor="@android:color/white" android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
4+
5+
</vector>

WordPress/src/main/res/values-ar/strings.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ Language: ar
2525
<string name="notifications_empty_subscribers">لا يوجد مشتركون حتى الآن</string>
2626
<string name="stats_view_emails">رسائل البريد الإلكتروني</string>
2727
<string name="stats_view_subscribers">المشتركون</string>
28-
<string name="stats_view_subscribers_chart">المشتركون</string>
2928
<string name="stats_view_total_subscribers">إجمالي المشتركين</string>
3029
<string name="stats_view_subscriber_totals">إجمالي المشتركين</string>
3130
<string name="stats_list_item_with_two_values_description">%1$s: %2$s، %3$s: %4$s، %5$s: %6$s</string>

0 commit comments

Comments
 (0)