Skip to content
Closed
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
5 changes: 5 additions & 0 deletions app/smartphone/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />

<!-- USB Host permissions for encryption -->
<uses-feature
android:name="android.hardware.usb.host"
android:required="false" />

<application
android:name=".M3UApplication"
android:allowBackup="true"
Expand Down
24 changes: 18 additions & 6 deletions app/smartphone/src/main/java/com/m3u/smartphone/ui/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
Expand All @@ -47,26 +48,37 @@ import com.m3u.data.database.model.Channel
import com.m3u.data.service.MediaCommand
import com.m3u.smartphone.ui.business.channel.PlayerActivity
import com.m3u.smartphone.ui.business.playlist.components.ChannelGallery
import com.m3u.smartphone.ui.business.setting.USBLockScreen
import com.m3u.smartphone.ui.common.AppNavHost
import com.m3u.smartphone.ui.common.helper.LocalHelper
import com.m3u.smartphone.ui.material.components.Destination
import com.m3u.smartphone.ui.material.components.SnackHost
import com.m3u.smartphone.ui.material.model.LocalSpacing
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import javax.inject.Inject

@Composable
fun App(
modifier: Modifier = Modifier,
viewModel: AppViewModel = hiltViewModel(),
viewModel: AppViewModel = hiltViewModel()
) {
val navController = rememberNavController()
val usbKeyState by viewModel.usbKeyRepository.state.collectAsStateWithLifecycle()

AppImpl(
navController = navController,
channels = viewModel.channels,
modifier = modifier
)
// Check if database is locked
if (usbKeyState.isEncryptionEnabled && !usbKeyState.isDatabaseUnlocked) {
USBLockScreen(
deviceName = usbKeyState.deviceName,
modifier = modifier
)
} else {
AppImpl(
navController = navController,
channels = viewModel.channels,
modifier = modifier
)
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.work.WorkManager
import com.m3u.data.database.model.Channel
import com.m3u.data.repository.channel.ChannelRepository
import com.m3u.data.repository.playlist.PlaylistRepository
import com.m3u.data.repository.usbkey.USBKeyRepository
import com.m3u.data.worker.SubscriptionWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
Expand All @@ -26,6 +27,7 @@ class AppViewModel @Inject constructor(
private val playlistRepository: PlaylistRepository,
private val channelRepository: ChannelRepository,
private val workManager: WorkManager,
val usbKeyRepository: USBKeyRepository,
) : ViewModel() {
init {
refreshProgrammes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.m3u.i18n.R.string
import com.m3u.smartphone.ui.business.setting.components.CanvasBottomSheet
import com.m3u.smartphone.ui.business.setting.fragments.AppearanceFragment
import com.m3u.smartphone.ui.business.setting.fragments.OptionalFragment
import com.m3u.smartphone.ui.business.setting.fragments.SecurityFragment
import com.m3u.smartphone.ui.business.setting.fragments.SubscriptionsFragment
import com.m3u.smartphone.ui.business.setting.fragments.preferences.PreferencesFragment
import com.m3u.smartphone.ui.common.helper.Fob
Expand Down Expand Up @@ -155,6 +156,7 @@ private fun SettingScreen(
val playlistTitle = stringResource(string.feat_setting_playlist_management)
val appearanceTitle = stringResource(string.feat_setting_appearance)
val optionalTitle = stringResource(string.feat_setting_optional_features)
val securityTitle = stringResource(string.feat_setting_security)

val colorArgb by preferenceOf(PreferencesKeys.COLOR_ARGB)

Expand All @@ -165,12 +167,13 @@ private fun SettingScreen(
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, it)
}

LifecycleResumeEffect(destination, defaultTitle, playlistTitle, appearanceTitle) {
LifecycleResumeEffect(destination, defaultTitle, playlistTitle, appearanceTitle, securityTitle) {
Metadata.title = when (destination) {
SettingDestination.Default -> defaultTitle
SettingDestination.Playlists -> playlistTitle
SettingDestination.Appearance -> appearanceTitle
SettingDestination.Optional -> optionalTitle
SettingDestination.Security -> securityTitle
}
.title()
.let(::AnnotatedString)
Expand Down Expand Up @@ -225,6 +228,14 @@ private fun SettingScreen(
)
}
},
navigateToSecurity = {
coroutineScope.launch {
navigator.navigateTo(
pane = ListDetailPaneScaffoldRole.Detail,
contentKey = SettingDestination.Security
)
}
},
modifier = Modifier.fillMaxSize()
)
},
Expand Down Expand Up @@ -266,6 +277,13 @@ private fun SettingScreen(
)
}

SettingDestination.Security -> {
SecurityFragment(
contentPadding = contentPadding,
modifier = Modifier.fillMaxSize()
)
}

else -> {}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package com.m3u.smartphone.ui.business.setting

import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material.icons.rounded.Usb
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.m3u.i18n.R.string

@Composable
fun USBLockScreen(
deviceName: String? = null,
modifier: Modifier = Modifier
) {
// Pulsating animation for USB icon
val infiniteTransition = rememberInfiniteTransition(label = "usb_pulse")
val alpha by infiniteTransition.animateFloat(
initialValue = 0.3f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(1500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)

Box(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(32.dp),
modifier = Modifier
.padding(48.dp)
.widthIn(max = 600.dp)
) {
// Lock Icon
Icon(
imageVector = Icons.Rounded.Lock,
contentDescription = null,
modifier = Modifier.size(96.dp),
tint = MaterialTheme.colorScheme.primary
)

// Title
Text(
text = stringResource(string.feat_setting_usb_encryption_locked_title),
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center
)

// Description
Text(
text = stringResource(string.feat_setting_usb_encryption_locked_message),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)

Spacer(modifier = Modifier.height(24.dp))

// USB Prompt Card
OutlinedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Icon(
imageVector = Icons.Rounded.Usb,
contentDescription = null,
modifier = Modifier
.size(64.dp)
.alpha(alpha),
tint = MaterialTheme.colorScheme.tertiary
)

Spacer(modifier = Modifier.height(16.dp))

Text(
text = stringResource(string.feat_setting_usb_encryption_insert_usb),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.tertiary,
textAlign = TextAlign.Center
)

if (deviceName != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(string.feat_setting_usb_encryption_waiting_for_device, deviceName),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
}
}

Spacer(modifier = Modifier.height(16.dp))

// Warning Text
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Rounded.Lock,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(string.feat_setting_usb_encryption_no_access_without_key),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
modifier = Modifier.weight(1f)
)
}
}
}
}
Loading
Loading