diff options
author | RaindropsSys <raindrops@equestria.dev> | 2023-10-27 22:29:56 +0200 |
---|---|---|
committer | RaindropsSys <raindrops@equestria.dev> | 2023-10-27 22:29:56 +0200 |
commit | 4d4308c46d4f7801c657cc79d2243e1a81831334 (patch) | |
tree | a2e392e0af92b9a3ca3d1b5afb841640276e2294 /android/app/src/main/java | |
parent | 9f9d66afebc59c6c265c4424f7b8fb36d8876541 (diff) | |
download | mist-4d4308c46d4f7801c657cc79d2243e1a81831334.tar.gz mist-4d4308c46d4f7801c657cc79d2243e1a81831334.tar.bz2 mist-4d4308c46d4f7801c657cc79d2243e1a81831334.zip |
Updated 32 files, added 279 files, deleted 3 files and renamed 14 files (automated)
Diffstat (limited to 'android/app/src/main/java')
6 files changed, 571 insertions, 0 deletions
diff --git a/android/app/src/main/java/dev/equestria/mist/JavaScriptExtensions.kt b/android/app/src/main/java/dev/equestria/mist/JavaScriptExtensions.kt new file mode 100644 index 0000000..8cb1c90 --- /dev/null +++ b/android/app/src/main/java/dev/equestria/mist/JavaScriptExtensions.kt @@ -0,0 +1,184 @@ +package dev.equestria.mist + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.BitmapFactory +import android.support.v4.media.MediaMetadataCompat +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat +import android.util.Log +import android.view.View +import android.view.Window +import android.webkit.JavascriptInterface +import androidx.activity.ComponentActivity +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.view.WindowCompat +import androidx.media.app.NotificationCompat.MediaStyle +import java.io.IOException +import java.net.URL +import android.graphics.Bitmap +import android.webkit.WebView + + +fun convertPixelsToDp(context: Context, pixels: Float): Float { + val screenPixelDensity = context.resources.displayMetrics.density + val dpValue = pixels / screenPixelDensity + return dpValue +} + +@SuppressLint("InternalInsetResource", "DiscouragedApi") +fun getStatusBarHeight(activity: ComponentActivity): Float { + val resourceId = activity.resources.getIdentifier("status_bar_height", "dimen", "android") + return if (resourceId > 0) { + val statusBarHeight = activity.resources.getDimensionPixelSize(resourceId) + convertPixelsToDp(activity.applicationContext, statusBarHeight.toFloat()) + } else { + 0f + } +} + +@SuppressLint("InternalInsetResource", "DiscouragedApi") +fun getNavigationBarHeight(activity: ComponentActivity): Float { + val resourceId = activity.resources.getIdentifier("navigation_bar_height", "dimen", "android") + return if (resourceId > 0) { + val statusBarHeight = activity.resources.getDimensionPixelSize(resourceId) + convertPixelsToDp(activity.applicationContext, statusBarHeight.toFloat()) + } else { + 0f + } +} + +class JavaScriptExtensions(originalActivity: MainActivity, private val window: Window, private val view: WebView, private val intent: Intent) { + private val activity: MainActivity = originalActivity + private var initialStatusBar: Boolean = WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars + private val session: MediaSessionCompat = MediaSessionCompat(activity.applicationContext, "Player") + private val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(activity.applicationContext, "main") + .setSmallIcon(R.drawable.ic_stat_player) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentTitle("Mist") + private var mAlbumArt: Bitmap? = null + + @JavascriptInterface + fun removeNotification() { + with (NotificationManagerCompat.from(activity.applicationContext)) { + cancel(1) + } + } + + @JavascriptInterface + fun removeService() { + activity.stopService(intent) + } + + @JavascriptInterface + fun updateNotificationAlbumArt(albumArt: String) { + activity.startForegroundService(intent) + + try { + val url = URL(albumArt) + val image = BitmapFactory.decodeStream(url.openConnection().getInputStream()) + mAlbumArt = image + } catch (e: IOException) { + Log.e("NotificationAlbumArt", "Failed to fetch album art", e) + } + } + + @JavascriptInterface + fun setNotificationData(title: String, artist: String, album: String, position: Long, duration: Long, playing: Boolean, buffering: Boolean, shuffle: Boolean, repeat: Boolean) { + val playbackStateBuilder = PlaybackStateCompat.Builder() + val style = MediaStyle() + + val state = if (buffering) { + PlaybackStateCompat.STATE_BUFFERING + } else if (playing) { + PlaybackStateCompat.STATE_PLAYING + } else { + PlaybackStateCompat.STATE_PAUSED + } + val playbackSpeed = 1f + playbackStateBuilder.setState(state, position, playbackSpeed) + + playbackStateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE + or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + or PlaybackStateCompat.ACTION_SKIP_TO_NEXT + or PlaybackStateCompat.ACTION_SEEK_TO + or PlaybackStateCompat.ACTION_STOP + ) + + val builder = MediaMetadataCompat.Builder() + + builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) + builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) + builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album) + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, mAlbumArt) + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, mAlbumArt) + builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration) + + val callback = object: MediaSessionCompat.Callback() { + override fun onPlay() { + view.post { view.evaluateJavascript("window.playPause();", null) } + } + + override fun onPause() { + view.post { view.evaluateJavascript("window.playPause();", null) } + } + + override fun onStop() { + view.post { view.evaluateJavascript("window.stop();", null) } + } + + override fun onSkipToPrevious() { + view.post { view.evaluateJavascript("window.previous();", null) } + } + + override fun onSkipToNext() { + view.post { view.evaluateJavascript("window.next();", null) } + } + + override fun onSeekTo(pos: Long) { + view.post { view.evaluateJavascript("window.document.getElementById(\"player\").contentDocument.getElementById(\"player-audio\").currentTime = $pos / 1000;", null) } + } + } + + session.setCallback(callback) + session.setMetadata(builder.build()) + session.setPlaybackState(playbackStateBuilder.build()) + style.setMediaSession(session.sessionToken) + notificationBuilder.setStyle(style) + + val notification = notificationBuilder.build() + + with (NotificationManagerCompat.from(activity.applicationContext)) { + if (ActivityCompat.checkSelfPermission( + activity.applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + notify(1, notification) + } + } + + @JavascriptInterface + fun getNavigationBarHeight(): Float { + return getNavigationBarHeight(activity) + } + + @JavascriptInterface + fun getStatusBarHeight(): Float { + return getStatusBarHeight(activity) + } + + @JavascriptInterface + fun setStatusBarTheme(dark: Boolean) { + if (!initialStatusBar) return + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = dark + } +} + diff --git a/android/app/src/main/java/dev/equestria/mist/MainActivity.kt b/android/app/src/main/java/dev/equestria/mist/MainActivity.kt new file mode 100644 index 0000000..939feaa --- /dev/null +++ b/android/app/src/main/java/dev/equestria/mist/MainActivity.kt @@ -0,0 +1,203 @@ +package dev.equestria.mist + +import android.Manifest +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.media.MediaMetadata +import android.os.Build +import android.os.Bundle +import android.support.v4.media.MediaMetadataCompat +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat +import android.util.Log +import android.view.ViewGroup +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.app.ActivityCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.media.app.NotificationCompat as MediaNotificationCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import com.android.volley.Request +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dev.equestria.mist.ui.theme.MistTheme + +class MainActivity : ComponentActivity() { + private lateinit var intent: Intent + + @OptIn(ExperimentalMaterial3Api::class) + fun showError() { + setContent { + AlertDialog( + onDismissRequest = { + finish() + } + ) { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Unable to connect to Mist at this moment. Please try again later.", + ) + Spacer(modifier = Modifier.height(24.dp)) + TextButton( + onClick = { + finish() + }, + modifier = Modifier.align(Alignment.End) + ) { + Text("Quit") + } + } + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + intent = Intent(this, MediaPlaybackService::class.java) + + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ActivityCompat.requestPermissions( + this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1 + ) + } + } + + val channel = NotificationChannel("main", "Playback", NotificationManager.IMPORTANCE_DEFAULT).apply {} + val notificationManager: NotificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + + setContent { + Box() { + MistTheme {} + } + } + + val volleyQueue = Volley.newRequestQueue(baseContext) + + val jsonObjectRequest = JsonObjectRequest( + Request.Method.GET, "https://mist.equestria.horse/connectivitycheck.txt", null, + + { response -> + Log.i("HTTPRequest", response.toString()) + + if (response.getString("status") == "OK") { + setContent { + Box() { + MistTheme { + WebViewContainer(this@MainActivity, intent) + } + } + } + + WebView.setWebContentsDebuggingEnabled(true) + } else { + setContent { + Box() { + MistTheme {} + } + } + showError() + } + }, + + { error -> + setContent { + Box() { + MistTheme {} + } + } + showError() + Log.e("HTTPRequest", "Request error: ${error.localizedMessage}") + }) + + volleyQueue.add(jsonObjectRequest) + + Log.d("StatusBarHeight", getStatusBarHeight(this).toString()) + Log.d("NavigationBarHeight", getNavigationBarHeight(this).toString()) + } +} + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun WebViewContainer(activity: MainActivity, intent: Intent) { + val mUrl = "https://mist.equestria.horse/app/" + + AndroidView(factory = { + WebView(it).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + clearCache(true) + clearHistory() + webViewClient = WebViewClient() + + settings.domStorageEnabled = true + settings.javaScriptEnabled = true + settings.safeBrowsingEnabled = false + settings.mediaPlaybackRequiresUserGesture = false + settings.userAgentString += " MistAndroid/" + BuildConfig.VERSION_NAME + settings.cacheMode = WebSettings.LOAD_NO_CACHE + + addJavascriptInterface(JavaScriptExtensions(activity, activity.window, this, intent), "MistAndroid") + loadUrl(mUrl) + } + }) +}
\ No newline at end of file diff --git a/android/app/src/main/java/dev/equestria/mist/MediaPlaybackService.kt b/android/app/src/main/java/dev/equestria/mist/MediaPlaybackService.kt new file mode 100644 index 0000000..15472b1 --- /dev/null +++ b/android/app/src/main/java/dev/equestria/mist/MediaPlaybackService.kt @@ -0,0 +1,59 @@ +package dev.equestria.mist + +import android.Manifest +import android.app.ForegroundServiceStartNotAllowedException +import android.app.Notification +import android.app.Service +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.IBinder +import android.support.v4.media.MediaMetadataCompat +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.ServiceCompat + +class MediaPlaybackService: Service() { + private lateinit var mNotification: Notification + + fun setNotification(notification: Notification) { + mNotification = notification + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val session = MediaSessionCompat(applicationContext, "Player") + val playbackStateBuilder = PlaybackStateCompat.Builder() + val style = androidx.media.app.NotificationCompat.MediaStyle() + + val notificationBuilder = NotificationCompat.Builder(applicationContext, "main") + .setSmallIcon(R.drawable.ic_stat_player) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentTitle("Mist") + + playbackStateBuilder.setState(PlaybackStateCompat.STATE_STOPPED, 0L, 1f) + + session.setPlaybackState(playbackStateBuilder.build()) + style.setMediaSession(session.sessionToken) + notificationBuilder.setStyle(style) + + val notification = notificationBuilder.build() + startForeground(1, notification) + return START_STICKY + } + + override fun onBind(p0: Intent?): IBinder? { + return null + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + with (NotificationManagerCompat.from(applicationContext)) { + cancel(1) + } + android.os.Process.killProcess(android.os.Process.myPid()) + stopSelf() + } +}
\ No newline at end of file diff --git a/android/app/src/main/java/dev/equestria/mist/ui/theme/Color.kt b/android/app/src/main/java/dev/equestria/mist/ui/theme/Color.kt new file mode 100644 index 0000000..4ef2c56 --- /dev/null +++ b/android/app/src/main/java/dev/equestria/mist/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package dev.equestria.mist.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260)
\ No newline at end of file diff --git a/android/app/src/main/java/dev/equestria/mist/ui/theme/Theme.kt b/android/app/src/main/java/dev/equestria/mist/ui/theme/Theme.kt new file mode 100644 index 0000000..051d910 --- /dev/null +++ b/android/app/src/main/java/dev/equestria/mist/ui/theme/Theme.kt @@ -0,0 +1,80 @@ +package dev.equestria.mist.ui.theme + +import android.app.Activity +import android.graphics.Color +import android.os.Build +import android.view.Window +import android.view.WindowInsetsController +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun MistTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + + val navigationBarItemColor = when { + darkTheme -> Window.DECOR_CAPTION_SHADE_LIGHT + else -> Window.DECOR_CAPTION_SHADE_DARK + } + + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = Color.TRANSPARENT + window.navigationBarColor = Color.TRANSPARENT + window.setDecorCaptionShade(navigationBarItemColor) + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +}
\ No newline at end of file diff --git a/android/app/src/main/java/dev/equestria/mist/ui/theme/Type.kt b/android/app/src/main/java/dev/equestria/mist/ui/theme/Type.kt new file mode 100644 index 0000000..290c7d2 --- /dev/null +++ b/android/app/src/main/java/dev/equestria/mist/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package dev.equestria.mist.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +)
\ No newline at end of file |