mirror of
https://gitlab.futo.org/keyboard/latinime.git
synced 2024-09-28 14:54:30 +01:00
Add model downloader activity for voice input
This commit is contained in:
parent
94c606718d
commit
2088909f88
@ -163,6 +163,8 @@ dependencies {
|
|||||||
implementation 'ch.acra:acra-http:5.11.1'
|
implementation 'ch.acra:acra-http:5.11.1'
|
||||||
implementation 'ch.acra:acra-dialog:5.11.1'
|
implementation 'ch.acra:acra-dialog:5.11.1'
|
||||||
|
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||||
|
|
||||||
implementation project(":voiceinput-shared")
|
implementation project(":voiceinput-shared")
|
||||||
|
|
||||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||||
|
@ -100,6 +100,14 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".uix.voiceinput.downloader.DownloadActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/model_downloader"
|
||||||
|
android:clearTaskOnLaunch="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar" />
|
||||||
|
|
||||||
<activity android:name=".permissions.PermissionsActivity"
|
<activity android:name=".permissions.PermissionsActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
@ -11,4 +11,14 @@
|
|||||||
<string name="dynamic_system_theme_name">Dynamic System</string>
|
<string name="dynamic_system_theme_name">Dynamic System</string>
|
||||||
<string name="dynamic_light_theme_name">Dynamic Light</string>
|
<string name="dynamic_light_theme_name">Dynamic Light</string>
|
||||||
<string name="dynamic_dark_theme_name">Dynamic Dark</string>
|
<string name="dynamic_dark_theme_name">Dynamic Dark</string>
|
||||||
|
|
||||||
|
<string name="download_required">Voice Input Model Downloader</string>
|
||||||
|
<string name="download_required_body">Download of some model files is necessary for voice input functionality. This may incur data fees if you are using Mobile Data instead of Wi-Fi.</string>
|
||||||
|
<string name="continue_">Continue</string>
|
||||||
|
|
||||||
|
<string name="download_progress">Voice Input Model Download Progress</string>
|
||||||
|
<string name="download_failed">Download of one or more resources has failed. Please make sure you\'re connected to a network, the app has network permission, or try again later.</string>
|
||||||
|
<string name="download_in_progress">Downloading models…</string>
|
||||||
|
|
||||||
|
<string name="model_downloader">Model Downloader</string>
|
||||||
</resources>
|
</resources>
|
@ -85,12 +85,12 @@ import org.futo.inputmethod.latin.inputlogic.InputLogic;
|
|||||||
import org.futo.inputmethod.latin.permissions.PermissionsManager;
|
import org.futo.inputmethod.latin.permissions.PermissionsManager;
|
||||||
import org.futo.inputmethod.latin.personalization.PersonalizationHelper;
|
import org.futo.inputmethod.latin.personalization.PersonalizationHelper;
|
||||||
import org.futo.inputmethod.latin.settings.Settings;
|
import org.futo.inputmethod.latin.settings.Settings;
|
||||||
import org.futo.inputmethod.latin.settings.SettingsActivity;
|
|
||||||
import org.futo.inputmethod.latin.settings.SettingsValues;
|
import org.futo.inputmethod.latin.settings.SettingsValues;
|
||||||
import org.futo.inputmethod.latin.suggestions.SuggestionStripView;
|
import org.futo.inputmethod.latin.suggestions.SuggestionStripView;
|
||||||
import org.futo.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
|
import org.futo.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
|
||||||
import org.futo.inputmethod.latin.touchinputconsumer.GestureConsumer;
|
import org.futo.inputmethod.latin.touchinputconsumer.GestureConsumer;
|
||||||
import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner;
|
import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner;
|
||||||
|
import org.futo.inputmethod.latin.uix.settings.SettingsActivity;
|
||||||
import org.futo.inputmethod.latin.utils.ApplicationUtils;
|
import org.futo.inputmethod.latin.utils.ApplicationUtils;
|
||||||
import org.futo.inputmethod.latin.utils.DialogUtils;
|
import org.futo.inputmethod.latin.utils.DialogUtils;
|
||||||
import org.futo.inputmethod.latin.utils.ImportantNoticeUtils;
|
import org.futo.inputmethod.latin.utils.ImportantNoticeUtils;
|
||||||
@ -1803,8 +1803,8 @@ public class LatinIMELegacy implements KeyboardActionListener,
|
|||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
intent.putExtra(SettingsActivity.EXTRA_SHOW_HOME_AS_UP, false);
|
//intent.putExtra(SettingsActivity.EXTRA_SHOW_HOME_AS_UP, false);
|
||||||
intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY, extraEntryValue);
|
//intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY, extraEntryValue);
|
||||||
startActivityOnTheSameDisplay(intent);
|
startActivityOnTheSameDisplay(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1832,7 +1832,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
|
|||||||
startActivityOnTheSameDisplay(intent);
|
startActivityOnTheSameDisplay(intent);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
launchSettings(SettingsActivity.EXTRA_ENTRY_VALUE_LONG_PRESS_COMMA);
|
launchSettings("");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2012 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 org.futo.inputmethod.latin.settings;
|
|
||||||
|
|
||||||
import org.futo.inputmethod.latin.permissions.PermissionsManager;
|
|
||||||
import org.futo.inputmethod.latin.utils.FragmentUtils;
|
|
||||||
import org.futo.inputmethod.latin.utils.StatsUtils;
|
|
||||||
import org.futo.inputmethod.latin.utils.StatsUtilsManager;
|
|
||||||
|
|
||||||
import android.app.ActionBar;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.PreferenceActivity;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
|
|
||||||
public final class SettingsActivity extends PreferenceActivity
|
|
||||||
implements ActivityCompat.OnRequestPermissionsResultCallback {
|
|
||||||
private static final String DEFAULT_FRAGMENT = SettingsFragment.class.getName();
|
|
||||||
|
|
||||||
public static final String EXTRA_SHOW_HOME_AS_UP = "show_home_as_up";
|
|
||||||
public static final String EXTRA_ENTRY_KEY = "entry";
|
|
||||||
public static final String EXTRA_ENTRY_VALUE_LONG_PRESS_COMMA = "long_press_comma";
|
|
||||||
public static final String EXTRA_ENTRY_VALUE_APP_ICON = "app_icon";
|
|
||||||
public static final String EXTRA_ENTRY_VALUE_NOTICE_DIALOG = "important_notice";
|
|
||||||
public static final String EXTRA_ENTRY_VALUE_SYSTEM_SETTINGS = "system_settings";
|
|
||||||
|
|
||||||
private boolean mShowHomeAsUp;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(final Bundle savedState) {
|
|
||||||
super.onCreate(savedState);
|
|
||||||
final ActionBar actionBar = getActionBar();
|
|
||||||
final Intent intent = getIntent();
|
|
||||||
if (actionBar != null) {
|
|
||||||
mShowHomeAsUp = intent.getBooleanExtra(EXTRA_SHOW_HOME_AS_UP, true);
|
|
||||||
actionBar.setDisplayHomeAsUpEnabled(mShowHomeAsUp);
|
|
||||||
actionBar.setHomeButtonEnabled(mShowHomeAsUp);
|
|
||||||
}
|
|
||||||
StatsUtils.onSettingsActivity(
|
|
||||||
intent.hasExtra(EXTRA_ENTRY_KEY) ? intent.getStringExtra(EXTRA_ENTRY_KEY)
|
|
||||||
: EXTRA_ENTRY_VALUE_SYSTEM_SETTINGS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
|
||||||
if (mShowHomeAsUp && item.getItemId() == android.R.id.home) {
|
|
||||||
finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Intent getIntent() {
|
|
||||||
final Intent intent = super.getIntent();
|
|
||||||
final String fragment = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
|
|
||||||
if (fragment == null) {
|
|
||||||
intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
|
|
||||||
}
|
|
||||||
intent.putExtra(EXTRA_NO_HEADERS, true);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isValidFragment(final String fragmentName) {
|
|
||||||
return FragmentUtils.isValidFragment(fragmentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
|
||||||
PermissionsManager.get(this).onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
}
|
|
||||||
}
|
|
@ -29,6 +29,7 @@ import android.view.MenuItem;
|
|||||||
|
|
||||||
import org.futo.inputmethod.latin.R;
|
import org.futo.inputmethod.latin.R;
|
||||||
import org.futo.inputmethod.latin.define.ProductionFlags;
|
import org.futo.inputmethod.latin.define.ProductionFlags;
|
||||||
|
import org.futo.inputmethod.latin.uix.settings.SettingsActivity;
|
||||||
import org.futo.inputmethod.latin.utils.ApplicationUtils;
|
import org.futo.inputmethod.latin.utils.ApplicationUtils;
|
||||||
import org.futo.inputmethod.latin.utils.FeedbackUtils;
|
import org.futo.inputmethod.latin.utils.FeedbackUtils;
|
||||||
import org.futo.inputmethodcommon.InputMethodSettingsFragment;
|
import org.futo.inputmethodcommon.InputMethodSettingsFragment;
|
||||||
|
@ -36,7 +36,7 @@ import android.widget.VideoView;
|
|||||||
import org.futo.inputmethod.compat.TextViewCompatUtils;
|
import org.futo.inputmethod.compat.TextViewCompatUtils;
|
||||||
import org.futo.inputmethod.compat.ViewCompatUtils;
|
import org.futo.inputmethod.compat.ViewCompatUtils;
|
||||||
import org.futo.inputmethod.latin.R;
|
import org.futo.inputmethod.latin.R;
|
||||||
import org.futo.inputmethod.latin.settings.SettingsActivity;
|
import org.futo.inputmethod.latin.uix.settings.SettingsActivity;
|
||||||
import org.futo.inputmethod.latin.utils.LeakGuardHandlerWrapper;
|
import org.futo.inputmethod.latin.utils.LeakGuardHandlerWrapper;
|
||||||
import org.futo.inputmethod.latin.utils.UncachedInputMethodManagerUtils;
|
import org.futo.inputmethod.latin.utils.UncachedInputMethodManagerUtils;
|
||||||
|
|
||||||
@ -265,8 +265,8 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
|
|||||||
intent.setClass(this, SettingsActivity.class);
|
intent.setClass(this, SettingsActivity.class);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY,
|
//intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY,
|
||||||
SettingsActivity.EXTRA_ENTRY_VALUE_APP_ICON);
|
// SettingsActivity.EXTRA_ENTRY_VALUE_APP_ICON);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.futo.inputmethod.latin.uix.actions
|
package org.futo.inputmethod.latin.uix.actions
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@ -12,6 +14,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@ -34,6 +37,7 @@ import org.futo.inputmethod.latin.uix.MULTILINGUAL_MODEL_INDEX
|
|||||||
import org.futo.inputmethod.latin.uix.PersistentActionState
|
import org.futo.inputmethod.latin.uix.PersistentActionState
|
||||||
import org.futo.inputmethod.latin.uix.VERBOSE_PROGRESS
|
import org.futo.inputmethod.latin.uix.VERBOSE_PROGRESS
|
||||||
import org.futo.inputmethod.latin.uix.getSetting
|
import org.futo.inputmethod.latin.uix.getSetting
|
||||||
|
import org.futo.inputmethod.latin.uix.voiceinput.downloader.DownloadActivity
|
||||||
import org.futo.voiceinput.shared.ENGLISH_MODELS
|
import org.futo.voiceinput.shared.ENGLISH_MODELS
|
||||||
import org.futo.voiceinput.shared.MULTILINGUAL_MODELS
|
import org.futo.voiceinput.shared.MULTILINGUAL_MODELS
|
||||||
import org.futo.voiceinput.shared.ModelDoesNotExistException
|
import org.futo.voiceinput.shared.ModelDoesNotExistException
|
||||||
@ -155,10 +159,18 @@ private class VoiceInputActionWindow(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ModelDownloader(modelException: ModelDoesNotExistException) {
|
private fun ModelDownloader(modelException: ModelDoesNotExistException) {
|
||||||
Column {
|
val context = LocalContext.current
|
||||||
Text("Model Download Required")
|
Box(modifier = Modifier.fillMaxSize().clickable {
|
||||||
Text("Not yet implemented")
|
val intent = Intent(context, DownloadActivity::class.java)
|
||||||
// TODO
|
intent.putStringArrayListExtra("models", ArrayList(modelException.models.map { model -> model.getRequiredDownloadList(context) }.flatten()))
|
||||||
|
|
||||||
|
if(context !is Activity) {
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(intent)
|
||||||
|
}) {
|
||||||
|
Text("Tap to complete setup", modifier = Modifier.align(Alignment.Center))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,431 @@
|
|||||||
|
package org.futo.inputmethod.latin.uix.voiceinput.downloader
|
||||||
|
|
||||||
|
// TODO: Rework this
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.futo.inputmethod.latin.R
|
||||||
|
import org.futo.inputmethod.latin.uix.THEME_KEY
|
||||||
|
import org.futo.inputmethod.latin.uix.deferGetSetting
|
||||||
|
import org.futo.inputmethod.latin.uix.settings.useDataStore
|
||||||
|
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||||
|
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
|
||||||
|
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
|
||||||
|
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
|
||||||
|
import org.futo.voiceinput.shared.ui.theme.Typography
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
|
||||||
|
data class ModelInfo(
|
||||||
|
val name: String,
|
||||||
|
val url: String,
|
||||||
|
var size: Long?,
|
||||||
|
var progress: Float = 0.0f,
|
||||||
|
var error: Boolean = false,
|
||||||
|
var finished: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
val EXAMPLE_MODELS = listOf(
|
||||||
|
ModelInfo(
|
||||||
|
name = "tiny-encoder-xatn.tflite",
|
||||||
|
url = "example.com",
|
||||||
|
size = 56L * 1024L * 1024L,
|
||||||
|
progress = 0.5f,
|
||||||
|
error = true
|
||||||
|
),
|
||||||
|
ModelInfo(
|
||||||
|
name = "tiny-decoder.tflite",
|
||||||
|
url = "example.com",
|
||||||
|
size = 73L * 1024L * 1024L,
|
||||||
|
progress = 0.3f,
|
||||||
|
error = false
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModelItem(model: ModelInfo, showProgress: Boolean) {
|
||||||
|
Column(modifier = Modifier.padding(4.dp)) {
|
||||||
|
val color = if (model.error) {
|
||||||
|
MaterialTheme.colorScheme.errorContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
}
|
||||||
|
Surface(modifier = Modifier, color = color, shape = RoundedCornerShape(4.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
if (model.error) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Warning, contentDescription = "Failed", modifier = Modifier
|
||||||
|
.align(CenterVertically)
|
||||||
|
.padding(4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val size = if (model.size != null) {
|
||||||
|
"%.1f".format(model.size!!.toFloat() / 1000000.0f)
|
||||||
|
} else {
|
||||||
|
"?"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(model.name, style = Typography.bodyLarge)
|
||||||
|
Text(
|
||||||
|
"$size MB",
|
||||||
|
style = Typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
if (showProgress && !model.error) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = model.progress, modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(0.dp, 8.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScreenTitle(title: String, showBack: Boolean = false, navController: NavHostController = rememberNavController()) {
|
||||||
|
val rowModifier = if(showBack) {
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { navController.popBackStack() }
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
}
|
||||||
|
Row(modifier = rowModifier) {
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
if(showBack) {
|
||||||
|
Icon(Icons.Default.ArrowBack, contentDescription = "Back", modifier = Modifier.align(CenterVertically))
|
||||||
|
Spacer(modifier = Modifier.width(18.dp))
|
||||||
|
}
|
||||||
|
Text(title, style = Typography.titleLarge, modifier = Modifier
|
||||||
|
.align(CenterVertically)
|
||||||
|
.padding(0.dp, 16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Composable
|
||||||
|
fun ScrollableList(content: @Composable () -> Unit) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun DownloadPrompt(
|
||||||
|
onContinue: () -> Unit = {},
|
||||||
|
onCancel: () -> Unit = {},
|
||||||
|
models: List<ModelInfo> = EXAMPLE_MODELS
|
||||||
|
) {
|
||||||
|
ScrollableList {
|
||||||
|
ScreenTitle(title = stringResource(R.string.download_required))
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.download_required_body),
|
||||||
|
style = Typography.bodyMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
for(model in models) {
|
||||||
|
ModelItem(model, showProgress = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Button(
|
||||||
|
onClick = onCancel, colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondary
|
||||||
|
), modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.weight(1.0f)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = onContinue, modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.weight(1.5f)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.continue_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun DownloadScreen(models: List<ModelInfo> = EXAMPLE_MODELS) {
|
||||||
|
ScrollableList {
|
||||||
|
ScreenTitle(stringResource(R.string.download_progress))
|
||||||
|
if (models.any { it.error }) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.download_failed),
|
||||||
|
style = Typography.bodyMedium
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.download_in_progress),
|
||||||
|
style = Typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
for(model in models) {
|
||||||
|
ModelItem(model, showProgress = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.fileNeedsDownloading(file: String): Boolean {
|
||||||
|
return !File(this.filesDir, file).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadActivity : ComponentActivity() {
|
||||||
|
private lateinit var modelsToDownload: List<ModelInfo>
|
||||||
|
private val httpClient = OkHttpClient()
|
||||||
|
private var isDownloading = false
|
||||||
|
|
||||||
|
private val themeOption: MutableState<ThemeOption?> = mutableStateOf(null)
|
||||||
|
private fun updateContent() {
|
||||||
|
setContent {
|
||||||
|
themeOption.value?.let { themeOption ->
|
||||||
|
val themeIdx = useDataStore(key = THEME_KEY.key, default = themeOption.key)
|
||||||
|
val theme: ThemeOption = ThemeOptions[themeIdx.value] ?: themeOption
|
||||||
|
UixThemeWrapper(theme.obtainColors(LocalContext.current)) {
|
||||||
|
// A surface container using the 'background' color from the theme
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
|
) {
|
||||||
|
if (isDownloading) {
|
||||||
|
DownloadScreen(models = modelsToDownload)
|
||||||
|
} else {
|
||||||
|
DownloadPrompt(
|
||||||
|
onContinue = { startDownload() },
|
||||||
|
onCancel = { cancel() },
|
||||||
|
models = modelsToDownload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startDownload() {
|
||||||
|
isDownloading = true
|
||||||
|
updateContent()
|
||||||
|
|
||||||
|
modelsToDownload.forEach {
|
||||||
|
val request = Request.Builder().method("GET", null).url(it.url).build()
|
||||||
|
|
||||||
|
httpClient.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
it.error = true
|
||||||
|
updateContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
response.body?.source()?.let { source ->
|
||||||
|
|
||||||
|
try {
|
||||||
|
it.size = response.headers["content-length"]!!.toLong()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("url failed ${it.url}")
|
||||||
|
println(response.headers)
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileName = it.name + ".download"
|
||||||
|
val file =
|
||||||
|
File.createTempFile(fileName, null, this@DownloadActivity.cacheDir)
|
||||||
|
val os = file.outputStream()
|
||||||
|
|
||||||
|
val buffer = ByteArray(128 * 1024)
|
||||||
|
var downloaded = 0
|
||||||
|
while (true) {
|
||||||
|
val read = source.read(buffer)
|
||||||
|
if (read == -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
os.write(buffer.sliceArray(0 until read))
|
||||||
|
|
||||||
|
downloaded += read
|
||||||
|
|
||||||
|
if (it.size != null) {
|
||||||
|
it.progress = downloaded.toFloat() / it.size!!.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
updateContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it.finished = true
|
||||||
|
it.progress = 1.0f
|
||||||
|
os.flush()
|
||||||
|
os.close()
|
||||||
|
|
||||||
|
assert(file.renameTo(File(this@DownloadActivity.filesDir, it.name)))
|
||||||
|
|
||||||
|
if (modelsToDownload.all { a -> a.finished }) {
|
||||||
|
downloadsFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancel() {
|
||||||
|
val returnIntent = Intent()
|
||||||
|
setResult(RESULT_CANCELED, returnIntent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadsFinished() {
|
||||||
|
val returnIntent = Intent()
|
||||||
|
setResult(RESULT_OK, returnIntent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun obtainModelSizes() {
|
||||||
|
modelsToDownload.forEach {
|
||||||
|
val request =
|
||||||
|
Request.Builder().method("HEAD", null).header("accept-encoding", "identity")
|
||||||
|
.url(it.url).build()
|
||||||
|
|
||||||
|
httpClient.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
it.error = true
|
||||||
|
updateContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
try {
|
||||||
|
it.size = response.headers["content-length"]!!.toLong()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("url failed ${it.url}")
|
||||||
|
println(response.headers)
|
||||||
|
e.printStackTrace()
|
||||||
|
it.error = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.code != 200) {
|
||||||
|
println("Bad response code ${response.code}")
|
||||||
|
it.error = true
|
||||||
|
}
|
||||||
|
updateContent()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val models = intent.getStringArrayListExtra("models")
|
||||||
|
?: throw IllegalStateException("intent extra `models` must be specified for DownloadActivity")
|
||||||
|
|
||||||
|
modelsToDownload = models.distinct().filter { this.fileNeedsDownloading(it) }.map {
|
||||||
|
ModelInfo(
|
||||||
|
name = it,
|
||||||
|
url = "https://voiceinput.futo.org/VoiceInput/${it}",
|
||||||
|
size = null,
|
||||||
|
progress = 0.0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelsToDownload.isEmpty()) {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
isDownloading = false
|
||||||
|
|
||||||
|
deferGetSetting(THEME_KEY) {
|
||||||
|
val themeOptionFromSettings = ThemeOptions[it]
|
||||||
|
val themeOption = when {
|
||||||
|
themeOptionFromSettings == null -> VoiceInputTheme
|
||||||
|
!themeOptionFromSettings.available(this) -> VoiceInputTheme
|
||||||
|
else -> themeOptionFromSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
this.themeOption.value = themeOption
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContent()
|
||||||
|
obtainModelSizes()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user