mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-03 14:46:57 +02:00
feat(widgets): add native Android and iOS home screen players
Publish artwork, metadata, and transport state through a shared Dart bridge. Add configurable Android RemoteViews and iOS WidgetKit players with controls and links into the full player. Serialize widget commands and session restoration, and share one headless-capable iOS Flutter engine with the foreground scene. Include native integration tests, Dart bridge tests, and widget setup documentation.
This commit is contained in:
1 parent
64445a09fc
commit
040c328479
34 files changed
+1601
-16
No files matched your search
@@ -0,0 +1,97 @@
|
||||
package com.zarz.spotiflac
|
||||
|
||||
import android.Manifest
|
||||
import android.appwidget.AppWidgetHost
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.File
|
||||
|
||||
/** Applies real RemoteViews through an Android widget host, including resizing. */
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PlayerWidgetTest {
|
||||
@Test
|
||||
fun launcherRendersEverySizeAndAppearance() {
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
val context = instrumentation.targetContext
|
||||
val manager = AppWidgetManager.getInstance(context)
|
||||
val prefs = PlayerWidgetProvider.preferences(context)
|
||||
val previousState = prefs.getString("state", null)
|
||||
val title = "Widget rendering test"
|
||||
val state = JSONObject().apply {
|
||||
put("title", title)
|
||||
put("artist", "Test artist")
|
||||
put("canPlay", true)
|
||||
put("playing", true)
|
||||
put("canNext", true)
|
||||
put("canPrevious", false)
|
||||
put("background", 0xff6b3945L)
|
||||
}
|
||||
prefs.edit().putString("state", state.toString()).commit()
|
||||
instrumentation.uiAutomation.adoptShellPermissionIdentity(Manifest.permission.BIND_APPWIDGET)
|
||||
try {
|
||||
instrumentation.runOnMainSync {
|
||||
val host = AppWidgetHost(context, 595)
|
||||
val id = host.allocateAppWidgetId()
|
||||
try {
|
||||
assertTrue(manager.bindAppWidgetIdIfAllowed(
|
||||
id, ComponentName(context, PlayerWidgetProvider::class.java),
|
||||
))
|
||||
val info = manager.getAppWidgetInfo(id)
|
||||
assertNotNull(info)
|
||||
for ((width, height) in listOf(140 to 156, 300 to 160, 320 to 350)) {
|
||||
manager.updateAppWidgetOptions(id, Bundle().apply {
|
||||
putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, width)
|
||||
putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, width)
|
||||
putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, height)
|
||||
putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, height)
|
||||
})
|
||||
for (theme in listOf("artwork", "light", "dark")) {
|
||||
prefs.edit().putString("theme_$id", theme).commit()
|
||||
PlayerWidgetProvider.update(context, intArrayOf(id))
|
||||
val view = host.createView(context, id, info)
|
||||
val titleView = view.findViewById<TextView>(R.id.player_widget_title)
|
||||
assertNotNull("RemoteViews failed at $width x $height, $theme", titleView)
|
||||
assertEquals(title, titleView.text.toString())
|
||||
assertTrue(view.findViewById<android.view.View>(R.id.player_widget_play).isEnabled)
|
||||
assertTrue(!view.findViewById<android.view.View>(R.id.player_widget_previous).isEnabled)
|
||||
val density = context.resources.displayMetrics.density
|
||||
val pixelsWide = (width * density).toInt()
|
||||
val pixelsHigh = (height * density).toInt()
|
||||
view.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(pixelsWide, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(pixelsHigh, View.MeasureSpec.EXACTLY),
|
||||
)
|
||||
view.layout(0, 0, pixelsWide, pixelsHigh)
|
||||
val bitmap = Bitmap.createBitmap(pixelsWide, pixelsHigh, Bitmap.Config.ARGB_8888)
|
||||
view.draw(Canvas(bitmap))
|
||||
File(context.cacheDir, "player-widget-$width-$height-$theme.png").outputStream().use {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||
}
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
host.deleteHost()
|
||||
prefs.edit().remove("theme_$id").remove("artist_$id").commit()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
instrumentation.uiAutomation.dropShellPermissionIdentity()
|
||||
prefs.edit().putString("state", previousState).commit()
|
||||
PlayerWidgetProvider.update(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user