Initial commit

This commit is contained in:
2019-11-09 13:37:43 +01:00
commit 2529ddf956
35 changed files with 1573 additions and 0 deletions

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="zoo.wbooster">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service android:name=".MediaPlayerService"/>
<service android:name=".DownloaderService"/>
<activity
android:name=".FullscreenActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/app_name"
android:theme="@style/FullscreenTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,124 @@
package zoo.wbooster
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.IBinder
import android.view.View
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_fullscreen.*
import java.net.URI
/**
* Full screen activity devoted to play music
*/
class FullscreenActivity : AppCompatActivity() {
private lateinit var mediaPlayerService: MediaPlayerService
private lateinit var downloaderService: DownloaderService
private var isDownloaderServiceBound: Boolean = false
private var isMediaPlayerServiceBound: Boolean = false
private val downloaderConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as DownloaderService.DownloaderBinder
downloaderService = binder.getService()
isDownloaderServiceBound = true
}
override fun onServiceDisconnected(name: ComponentName?) {
isDownloaderServiceBound = false
}
}
private val mediaPlayerConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as MediaPlayerService.MediaPlayerBinder
mediaPlayerService = binder.getService()
isMediaPlayerServiceBound = true
}
override fun onServiceDisconnected(name: ComponentName?) {
isMediaPlayerServiceBound = false
}
}
override fun onStart() {
super.onStart()
ContextCompat.startForegroundService(this, Intent(this, MediaPlayerService::class.java))
Intent(this, MediaPlayerService::class.java).also {
bindService(it, mediaPlayerConnection, Context.BIND_AUTO_CREATE)
}
Intent(this, DownloaderService::class.java).also {
bindService(it, downloaderConnection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
unbindService(mediaPlayerConnection)
unbindService(downloaderConnection)
isMediaPlayerServiceBound = false
isDownloaderServiceBound = false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Upgrade Youtube-dl
// YT.updateYoutubeDL(application)
// Set fullscreen immersive mode
setContentView(R.layout.activity_fullscreen)
fullscreen_content.systemUiVisibility =
View.SYSTEM_UI_FLAG_LOW_PROFILE or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
fullscreen_content_controls.visibility = View.VISIBLE
}
@RequiresApi(Build.VERSION_CODES.O)
fun startPlaylist(v: View){
playButton.setOnClickListener{} // Remove listner
playButton.isEnabled = false
if (!isMediaPlayerServiceBound || !isDownloaderServiceBound){
println("Problem connecting to services")
println("Media player status: $isMediaPlayerServiceBound")
println("Downloader status: $isDownloaderServiceBound")
return
}
// Setup title update lambda
mediaPlayerService.titleUpdater = {
runOnUiThread(Runnable {
currently_playing.text = " $it ".repeat(400)
currently_playing.isSelected = true
})
}
// Setup image update lambda
mediaPlayerService.thumbnailUpdater = {
runOnUiThread(Runnable {
thumbnail_view.setImageURI(Uri.parse(it))
})
}
downloaderService.startDownloading(mediaPlayerService, downloadBar)
mediaPlayerService.startPlaying()
}
}

View File

@ -0,0 +1,275 @@
package zoo.wbooster
import android.app.*
import android.content.Intent
import android.media.MediaPlayer
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.widget.ProgressBar
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import com.beust.klaxon.Klaxon
import com.vdurmont.emoji.EmojiParser
import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLException
import com.yausername.youtubedl_android.YoutubeDLRequest
import java.lang.Exception
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
class VideoUsefulDetails(
val path: String,
val title: String,
val thumbnail: String
)
class VideoInfo(
val _type: String,
val id: String,
val ie_key: String,
val title: String,
val url: String
)
class PlaylistInfos(
val _type: String,
val extractor: String,
val extractor_key: String,
val id: String,
val title: String,
val uploader: String,
val uploader_id: String,
val uploader_url: String,
val webpage_url: String,
val webpage_url_basename: String,
val entries: Array<VideoInfo>
){
fun getRandomVideo() = this.entries.get(Random().nextInt(this.entries.size))
}
class DownloaderService: Service() {
private var playlist: PlaylistInfos? = null
private val binder = DownloaderBinder()
private var isStarted = false
inner class DownloaderBinder: Binder() {
fun getService(): DownloaderService = this@DownloaderService
}
private fun customInit(){
if (isStarted) return
// Initialize Youtube-dl
// Thanks to https://github.com/yausername/youtubedl-android
try {
YT.init(application)
} catch (e: YoutubeDLException) {
Log.e("failed to initialize youtubedl-android", e.localizedMessage)
}
try {
FFmpeg.getInstance().init(application)
} catch (e: Exception) {
Log.e("failed to initialize FFmpeg", e.localizedMessage)
}
playlist = downloadPlaylistInfo()
isStarted = true
}
private fun downloadPlaylistInfo(): PlaylistInfos?{
val request = YoutubeDLRequest(PLAYLIST_URL)
request.setOption("--dump-single-json")
request.setOption("--flat-playlist")
try {
return Klaxon().parse<PlaylistInfos>(YT.execute(request).out)
} catch (e: Exception) {
println("Error downloading playlist infos")
println(e)
return null
}
}
fun startDownloading(mediaPlayerService: MediaPlayerService, downloadBar: ProgressBar) {
if (playlist == null){
println("No fucking playlist info")
return
}
Thread(Runnable {
while (true) {
val video = playlist?.getRandomVideo()
val url = video?.url ?: ""
val info = try {
YT.getInfo(url)
} catch(e: YoutubeDLException) {
println("Error getting infos for video, skipping")
println(e)
continue
}
val title = EmojiParser.removeAllEmojis(info.title)
if (title == null){
println("Error with music title, skipping")
continue
}
val pathTemplate = "${applicationContext.filesDir}/${title}--${video?.id}"
val filePath = "$pathTemplate.mp3"
val thumnailPath = "$pathTemplate.jpg"
val request = YoutubeDLRequest(url)
request.setOption("-x")
request.setOption("--audio-format", "mp3")
request.setOption("--write-thumbnail")
request.setOption("--cache-dir", "${applicationContext.filesDir}/.cache")
request.setOption("-o", "$pathTemplate.%(ext)s")
try {
YT.execute(request) { progress, etaInSeconds ->
println("$progress% (ETA $etaInSeconds seconds)")
downloadBar.progress = progress.toInt()
}
mediaPlayerService.addNextSong(VideoUsefulDetails(filePath, info.title, thumnailPath))
} catch (e: Exception) {
println(e)
}
}
}).start()
}
override fun onBind(intent: Intent?): IBinder? {
customInit()
return binder
}
companion object {
/**
* URL to the WBooster playlist
*/
private val PLAYLIST_URL =
"https://www.youtube.com/playlist?list=PLQC73oq6JtgyYNOeifrJXXzZ1-F0Kgmbg"
private val YT = YoutubeDL.getInstance()
}
}
class MediaPlayerService: Service(), MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener {
private lateinit var notificationManager: NotificationManager
private lateinit var notificationBuilder: NotificationCompat.Builder
private var playerThread: Thread? = null
private val binder = MediaPlayerBinder()
private var mMediaPlayer: MediaPlayer? = null
private var playlistQueue: Queue<VideoUsefulDetails> = ConcurrentLinkedQueue<VideoUsefulDetails>()
private var currentSong: VideoUsefulDetails? = null
var titleUpdater = {title: String -> }
var thumbnailUpdater = { path: String ->}
inner class MediaPlayerBinder: Binder() {
fun getService(): MediaPlayerService = this@MediaPlayerService
}
fun addNextSong(details: VideoUsefulDetails) {
this.playlistQueue.add(details)
}
/* Get next music path and update interface */
private fun updateNextSong(){
/* poll next song */
currentSong = playlistQueue.poll()
while (currentSong == null){
Thread.sleep(1000) /* Wait for another element */
currentSong = playlistQueue.poll()
}
// Update notification
notificationBuilder.setContentText(currentSong!!.title)
val notification = notificationBuilder.build()
notification.flags = Notification.FLAG_ONGOING_EVENT
notificationManager.notify(1, notificationBuilder.build())
// Update title
titleUpdater(currentSong!!.title)
// Update image
thumbnailUpdater(currentSong!!.thumbnail)
}
fun startPlaying() {
playerThread?.interrupt()
playerThread = Thread(Runnable {
updateNextSong()
/* launch media player */
mMediaPlayer = MediaPlayer().apply {
setDataSource(currentSong!!.path)
setOnPreparedListener(this@MediaPlayerService)
setOnCompletionListener(this@MediaPlayerService)
prepareAsync() // prepare async to not block main thread
}
})
/* Start media player thread */
playerThread?.start()
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Create Notification channel
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground music channel",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(serviceChannel)
val notificationIntent = Intent(this, AppCompatActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)
notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getText(R.string.app_name))
.setContentText("")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(pendingIntent)
startForeground(1, notificationBuilder.build())
return START_NOT_STICKY
}
override fun onBind(intent: Intent?) = binder
/** Called when MediaPlayer is ready */
override fun onPrepared(mediaPlayer: MediaPlayer) {
mediaPlayer.start()
}
override fun onCompletion(mp: MediaPlayer?) {
updateNextSong()
mp?.reset()
mp?.setDataSource(currentSong!!.path)
mp?.prepareAsync()
}
override fun onDestroy() {
super.onDestroy()
mMediaPlayer?.release()
playerThread?.interrupt()
stopSelf()
}
companion object {
val CHANNEL_ID = "ForegroundMediaPlayer"
}
}

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#ff0099cc">
<TextView
android:id="@+id/fullscreen_content"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center"
android:keepScreenOn="true"
android:textColor="#ff33b5e5"
android:textSize="50.0sp"
android:textStyle="bold" />
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:fitsSystemWindows="true">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<ProgressBar
android:id="@+id/downloadBar"
style="?android:progressBarStyleHorizontal"
android:layout_width="fill_parent"
android:layout_height="28.0dip" />
<TextView
android:id="@+id/currently_playing"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:focusable="true"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@android:color/white" />
<ImageView
android:id="@+id/thumbnail_view"
tools:ignore="ContentDescription"
android:layout_width="fill_parent"
android:layout_height="496.0dip"
tools:srcCompat="@tools:sample/avatars" />
</LinearLayout>
<LinearLayout
android:id="@+id/fullscreen_content_controls"
style="?metaButtonBarStyle"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:background="@color/black_overlay"
android:orientation="horizontal">
<Button
android:id="@+id/playButton"
style="?metaButtonBarButtonStyle"
android:layout_width="0.0dip"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_weight="1.0"
android:onClick="startPlaylist"
android:text="@string/play_button" />
</LinearLayout>
</FrameLayout>
</FrameLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WBoosterAndroid</string>
<string name="play_button">Jouer</string>
</resources>

View File

@ -0,0 +1,12 @@
<resources>
<!-- Declare custom theme attributes that allow changing which styles are
used for button bars depending on the API level.
?android:attr/buttonBarStyle is new as of API 11 so this is
necessary to support previous API levels. -->
<declare-styleable name="ButtonBarContainerTheme">
<attr name="metaButtonBarStyle" format="reference" />
<attr name="metaButtonBarButtonStyle" format="reference" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
<color name="black_overlay">#66000000</color>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WBoosterAndroid</string>
<string name="play_button">Play</string>
</resources>

View File

@ -0,0 +1,23 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="FullscreenTheme" parent="AppTheme">
<item name="android:actionBarStyle">@style/FullscreenActionBarStyle</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="android:windowBackground">@null</item>
<item name="metaButtonBarStyle">?android:attr/buttonBarStyle</item>
<item name="metaButtonBarButtonStyle">?android:attr/buttonBarButtonStyle</item>
</style>
<style name="FullscreenActionBarStyle" parent="Widget.AppCompat.ActionBar">
<item name="android:background">@color/black_overlay</item>
</style>
</resources>