I am building a video player app for Android TV. I am using Exoplayer leanback dependency as explained in https://developer.android.com/training/tv/playback/transport-controls.
So far I've been able to display the video title, which is static, but I need to display a subtitle that is dynamic, it changes whenever the video playing changes. How can I do it?
The image below shows how the video player looks like. I've used a subtitle phrase as a placeholder on where it should appear.
I was able to solve the problem. I added a listener in the VideoSupportFragment class, Player.EventListener.
This way:
class VideoFragment(mediaItems: Map<String, Any>) : VideoSupportFragment(), Player.EventListener {
private var playerAdapter: ExoPlayerAdapter? = null
private var _mediaItems: Map<String, Any>? = null
private lateinit var mMediaPlayerGlue: VideoMediaPlayerGlue<ExoPlayerAdapter>
private var mItems: List<*>? = null
init {
_mediaItems = mediaItems
}
private val mHost: VideoSupportFragmentGlueHost = VideoSupportFragmentGlueHost(this)
private fun playWhenReady(glue: PlaybackGlue) {
if (glue.isPrepared) {
glue.play()
} else {
glue.addPlayerCallback(object : PlaybackGlue.PlayerCallback() {
override fun onPreparedStateChanged(glue: PlaybackGlue) {
if (glue.isPrepared) {
glue.removePlayerCallback(this);
glue.play()
}
}
})
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mediaSource = _mediaItems?.get("media_source") as Map<*, *>
playerAdapter = ExoPlayerAdapter(activity!!, _mediaItems!!)
mMediaPlayerGlue =
VideoMediaPlayerGlue(activity, playerAdapter!!)
mMediaPlayerGlue.host = mHost;
mMediaPlayerGlue.isControlsOverlayAutoHideEnabled = true
mItems = mediaSource["media_items"] as List<*>
mMediaPlayerGlue.title = mediaSource["title"] as CharSequence?
mMediaPlayerGlue.playerAdapter.setDataSource()
mMediaPlayerGlue.isSeekEnabled = true
playerAdapter?.player?.addListener(this);
playWhenReady(mMediaPlayerGlue)
}
override fun onPause() {
super.onPause()
playerAdapter?.player?.pause()
}
override fun onResume() {
super.onResume()
playerAdapter?.player?.play()
}
override fun onDestroy() {
super.onDestroy()
playerAdapter?.player?.removeListener(this);
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (mItems?.size!! > 1){
val item : Map<*, *> = mItems!![playerAdapter?.player?.currentWindowIndex!!] as Map<*, *>
mMediaPlayerGlue.subtitle = item["subtitle"] as String
}
}
}
Related
I am currently building a NewsApplication consisting of 7 different categories
App when working properly
The problem I am currently facing is, whenever I start the app, the app would send out 7 requests, however at times, some of the responses would result in the Sockettimeout error, which makes it awkward as some of the Fragments will be populated while the others will be blank.
I then tried a different method, I attempted to prevent any of the fragments from loading untill all of the responses are successful, however that will just leave me with a blank/Loading screen when one of the resonses suffer from a Sockettimeout error occurs.
**
Is there any way to force the app from displaying anything except for the error message when any of the responses suffer from an error?**
App when there is an error, like no internet connection or Sockettimeouterror
I am trying the find a way to block any fragments from loading when there is a Sockettimeout error and display the relevent Error Message.
Repository, I used the Callback interface to help me detect server side errors such as SocketTimeOutExeption
class NewsRepository(val db:RoomDatabases ) {
suspend fun upsert(article: Article) = db.getArticleDao().upsert(article)
fun getSavedNews() = db.getArticleDao().getAllArticles()
suspend fun deleteArticle(article: Article) = db.getArticleDao().deleteArticle(article)
suspend fun empty() = db.getArticleDao().isEmpty()
suspend fun nukeTable() = db.getArticleDao().nukeTable()
fun getNewsCall(country: String, Category: String?): MutableLiveData<MutableList<Article>> {
val call = RetrofitHelper.NewsApiCall.api.getNews(
country,
Category,
"5a3e054de1834138a2fbc4a75ee69053"
)
var Newlist = MutableLiveData<MutableList<Article>>()
call.enqueue(object : Callback<NewsDataFromJson> {
override fun onResponse(
call: Call<NewsDataFromJson>,
response: Response<NewsDataFromJson>
) {
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
Newlist.value = body.articles
}
} else {
val jsonObj: JSONObject?
jsonObj = response.errorBody()?.string().let { JSONObject(it) }
if (jsonObj != null) {
MainActivity.apiRequestError = true
MainActivity.errorMessage = jsonObj.getString("message")
Newlist.value = mutableListOf<Article>()
}
}
}
override fun onFailure(call: Call<NewsDataFromJson>, t: Throwable) {
MainActivity.apiRequestError = true
MainActivity.errorMessage = t.localizedMessage as String
Log.d("err_msg", "msg" + t.localizedMessage)
}
})
return Newlist
}
}
MainActivity, this is where I call the requests
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
requestNews(GENERAL, generalNews,"us")
requestNews(TECHNOLOGY,TechNews,"us")
requestNews(HEALTH,healthNews,"us")
requestNews(SPORTS, SportsNews,"us")
requestNews(ENTERTAINMENT, EntertainmentNews,"us")
requestNews(SCIENCE, ScienceNews,"us")
requestNews(BUSINESS, BusinessNews,"us")
}
private fun requestNews(newsCategory: String, newsData: MutableList<Article>,country:String) {
viewModel.getNews(category = newsCategory, Country = country)?.observe(this) {
newsData.addAll(it)
totalRequestCount += 1
if(!apiRequestError){
if(totalRequestCount == 7){
ProgresBar.visibility = View.GONE
ProgresBar.visibility = View.GONE
setViewPager()
}
}else if(apiRequestError){
ProgresBar.visibility = View.GONE
FragmentContainer.visibility = View.GONE
val showError: TextView = findViewById(R.id.display_error)
showError.text = errorMessage
showError.visibility = View.VISIBLE
}
}
}
companion object{
var ScienceNews: MutableList<Article> = mutableListOf()
var EntertainmentNews: MutableList<Article> = mutableListOf()
var SportsNews: MutableList<Article> = mutableListOf()
var BusinessNews: MutableList<Article> = mutableListOf()
var healthNews: MutableList<Article> = mutableListOf()
var generalNews: MutableList<Article> = mutableListOf()
var TechNews: MutableList<Article> = mutableListOf()
var apiRequestError = false
var errorMessage = "error"
var SocketTimeout: JSONException? = null
}
}
ViewPagingFragment, this is where the ViewPager lives and this is where the FragmentAdapter is connected to.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val Categories = arrayListOf<String>("BreakingNews","Technology","Health","Science","Entertainment","Sports","Business")
viewpager(Categories)
viewPagerView = view.findViewById(R.id.view_pager)
viewPagerView.offscreenPageLimit = 7
var MainToolbarSaved = requireActivity().findViewById<Toolbar>(R.id.MenuToolBar)
var SecondaryToolBarSaved = requireActivity().findViewById<Toolbar>(R.id.topAppBarthesecond)
var MenuSavedButton = requireActivity().findViewById<ImageButton>(R.id.MenuSavedButton)
MainToolbarSaved.visibility = View.VISIBLE
SecondaryToolBarSaved.visibility = View.GONE
MenuSavedButton.setOnClickListener {
this.findNavController().navigate(R.id.action_global_savedFragment)
}
}
fun viewpager(FragmentList:ArrayList<String>){
val tabLayout = binding.tabLayout
PagerAdapter = FragmentAdapter(childFragmentManager,lifecycle)
binding.viewPager.adapter = PagerAdapter
tabLayout.tabMode = TabLayout.MODE_SCROLLABLE
TabLayoutMediator(tabLayout, binding.viewPager) { tab, position ->
tab.text = FragmentList[position]
}.attach()
}
Any tips on how I can do this?
I have attempted to look through other people's project and looked through the documentations for viewpager just to name a few.
I was developing some Conference application and got confused on how to handle PARTICIPANT_LEFT.action in code. Here is my code
private var roomName: String? = null
private lateinit var anotherUser:String
private var username: String? = SharedUserObject.userID
private var jitsiMeetView: JitsiMeetView? = null
#SuppressLint("LogNotTimber")
#Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_call)
anotherUser = intent.getStringExtra("AnotherUser").toString()
SharedUserObject.checkCallActivity = true
// get data from Two Activities (IncomingCall , CallFrom )
// create jitsi call view
// if (!SharedUserObject.checkCallActivity) {
dataFromIntent
jitsiMeetView = JitsiMeetView(this#CallActivity)
val conferenceOptions = videoChatOptions
jitsiMeetView!!.join(conferenceOptions)
setContentView(jitsiMeetView)
jitsiMeetView!!.listener = this
// }
val intentFilter = IntentFilter()
intentFilter.addAction(BroadcastEvent.Type.PARTICIPANT_LEFT.action)
LocalBroadcastManager.getInstance(this).registerReceiver(BroadcastReceiver(applicationContext), intentFilter)
}
override fun requestPermissions(
strings: Array<String>,
i: Int,
permissionListener: PermissionListener
) {}
// this run when user accept the call and set it busy
#SuppressLint("LogNotTimber")
override fun onConferenceJoined(map: Map<String, Any>) {
SharedUserObject.inCall = true
}
#SuppressLint("LogNotTimber")
// this run when user end the call and set it not busy
override fun onConferenceTerminated(map: Map<String, Any>) {
SharedUserObject.inCall = false
//
SharedUserObject.checkCallActivity = false
jitsiMeetView?.leave()
jitsiMeetView?.dispose()
jitsiMeetView = null
finish()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
override fun onConferenceWillJoin(map: Map<String, Any>) {
//LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(muteBroadcastIntent)
}
private val dataFromIntent: Unit
get() {
roomName = intent.getStringExtra("RoomName").toString()
username = ""
}
// Set call subject here. Connection with jitsi call server and create Call.
private val videoChatOptions: JitsiMeetConferenceOptions
get() {
var videoChatUrl: URL? = null
try {
videoChatUrl = URL("https://meet.jit.si")
} catch (e: Exception) {
e.printStackTrace()
}
val meetUserInfo = JitsiMeetUserInfo()
meetUserInfo.displayName = SharedUserObject.userID
return JitsiMeetConferenceOptions.Builder()
.setServerURL(videoChatUrl)
.setAudioOnly(true)
.setAudioMuted(false)
.setUserInfo(meetUserInfo)
.setSubject(roomName) // Set call subject here. use to display phone number here.
.setRoom(roomName)
.build()
}
override fun onDestroy() {
super.onDestroy()
SharedUserObject.checkCallActivity = false
jitsiMeetView?.leave()
jitsiMeetView?.dispose()
jitsiMeetView = null
JitsiMeetActivityDelegate.onHostDestroy(this)
}
}
I need that when user leaves the Conference then the Conference should end. I tried using BroadcastEvent.Type.PARTICIPANT_LEFT.action
but no success was received. Please guide me if there is some other way to implement it.
I have created a meditation application that users can select the class they want from the recycler view within the fragment, then it will show the detailed content and steps in a new activity. In the new activity, I have implemented a audio playing functions where user can stream the audio file from the firebase. The problem now is whenever I switch back to the recycler view within the fragment, the audio file still plays in the background. How do I stop the audio file from playing whenever I switch to another page within the app or switch to another app ?
This is the code for the recycler view within the fragment
class ClassFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var classArrayList: ArrayList<Classes>
private lateinit var tempArraylist: ArrayList<Classes>
private lateinit var classAdapter: ClassAdapter
private lateinit var db: FirebaseFirestore
var mediaPlayer: MediaPlayer? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view: View = inflater.inflate(R.layout.fragment_class, container, false)
recyclerView = view.findViewById(R.id.ClassList)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.setHasFixedSize(true)
classArrayList = arrayListOf()
tempArraylist = arrayListOf()
eventChangeListener()
classAdapter = ClassAdapter(requireContext(), classArrayList)
recyclerView.adapter = classAdapter
return view
}
private fun eventChangeListener() {
db = FirebaseFirestore.getInstance()
db.collection("class").addSnapshotListener(object : EventListener<QuerySnapshot> {
override fun onEvent(value: QuerySnapshot?, error: FirebaseFirestoreException?) {
if (error != null) {
Log.e("Firestore error", error.message.toString())
return
}
for (dc: DocumentChange in value?.documentChanges!!) {
if (dc.type == DocumentChange.Type.ADDED) {
classArrayList.add(
dc.document.toObject(
(Classes::class.java)
)
)
}
}
tempArraylist.clear()
tempArraylist.addAll(classArrayList)
classAdapter.notifyDataSetChanged()
}
})
}
}
This is the code for the new activity page where it will show content when the user selects from recyclerview
class DetailActivity : AppCompatActivity() {
private lateinit var imageViewClass: ImageView
private lateinit var textViewClassName: TextView
private lateinit var textViewClassDes: TextView
private lateinit var textViewClassContent: TextView
private var stop:Boolean = false
var mediaPlayer: MediaPlayer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail)
val btnPlay: Button = findViewById(R.id.btnPlay)
val btnStop: Button = findViewById(R.id.btnStop)
imageViewClass = findViewById(R.id.imageViewClassImage)
textViewClassName = findViewById(R.id.textViewClassName)
textViewClassDes = findViewById(R.id.textViewClassDescription)
textViewClassContent = findViewById(R.id.textViewClassContent)
val classImage = intent.getStringExtra("class_image")
val className = intent.getStringExtra("class_name")
val classDes = intent.getStringExtra("class_des")
val classContent = intent.getStringExtra("class_content")
val classAudio = intent.getStringExtra("class_audio")
Glide.with(this)
.load(classImage)
.centerCrop()
.into(imageViewClass)
textViewClassName.text = className
textViewClassDes.text = classDes
textViewClassDes.text = classDes!!.replace("\\n", "\n")
textViewClassContent.text = classContent
textViewClassContent.text = classContent!!.replace("\\n", "\n")
btnPlay.isEnabled = true
btnStop.isEnabled = false
btnPlay.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer.create(this#DetailActivity, Uri.parse(classAudio))
mediaPlayer!!.isLooping = true
mediaPlayer!!.start()
btnStop.isEnabled = true
btnPlay.isEnabled = false
Toast.makeText(applicationContext,"Audio Starts",Toast.LENGTH_SHORT).show()
} else mediaPlayer!!.start()
}
})
mediaPlayer?.setOnCompletionListener {
btnPlay.isEnabled = true
btnStop.isEnabled = false
Toast.makeText(this,"end",Toast.LENGTH_SHORT).show()
}
btnStop.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
if(mediaPlayer!!.isPlaying){
stop = false
mediaPlayer!!.stop()
mediaPlayer!!.reset()
mediaPlayer!!.release()
mediaPlayer = null
btnPlay.isEnabled = true
btnStop.isEnabled = false
Toast.makeText(applicationContext,"Audio Stops",Toast.LENGTH_SHORT).show()
}
}
})
}
}
This is a classic case of onDestroy of the Activity being called. I'll keep it short but there is a lifecycle through which an Activity undergoes and when you seem to come back from the SecondActivity to the first Activity, the onDestroy of your second activity is called to perform the destruction function
So the solution, override the onDestroy method in your DetailActivity class and do something like this
override fun onDestroy(){
if(mediaPlayer != null){
mediaPlayer!!.stop()
mediaPlayer!!.reset()
mediaPlayer!!.release()
mediaPlayer = null
}
}
You can read in detail about Activity Lifecyle
I have an activity to perform rest API everytime it opened and i use MVVM pattern for this project. But with this snippet code i failed to get updated everytime i open activity. So i debug all my parameters in every line, they all fine the suspect problem might when apiService.readNewsAsync(param1,param2) execute, my postValue did not update my resulRead parameter. There were no crash here, but i got result which not updated from result (postValue). Can someone explain to me why this happened?
Here what activity looks like
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivityReadBinding>(this,
R.layout.activity_read).apply {
this.viewModel = readViewModel
this.lifecycleOwner = this#ReadActivity
}
readViewModel.observerRead.observe(this, Observer {
val sukses = it.isSuccess
when{
sukses -> {
val data = it.data as Read
val article = data.article
//Log.d("-->", "${article.toString()}")
}
else -> {
toast("ada error ${it.msg}")
Timber.d("ERROR : ${it.msg}")
}
}
})
readViewModel.getReadNews()
}
Viewmodel
var observerRead = MutableLiveData<AppResponse>()
init {
observerRead = readRepository.observerReadNews()
}
fun getReadNews() {
// kanal and guid i fetch from intent and these value are valid
loadingVisibility = View.VISIBLE
val ok = readRepository.getReadNews(kanal!!, guid!!)
if(ok){
loadingVisibility = View.GONE
}
}
REPOSITORY
class ReadRepositoryImpl private constructor(private val newsdataDao: NewsdataDao) : ReadRepository{
override fun observerReadNews(): MutableLiveData<AppResponse> {
return newsdataDao.resultRead
}
override fun getReadNews(channel: String, guid: Int) = newsdataDao.readNews(channel, guid)
companion object{
#Volatile private var instance: ReadRepositoryImpl? = null
fun getInstance(newsdataDao: NewsdataDao) = instance ?: synchronized(this){
instance ?: ReadRepositoryImpl(newsdataDao).also {
instance = it
}
}
}
}
MODEL / DATA SOURCE
class NewsdataDao {
private val apiService = ApiClient.getClient().create(ApiService::class.java)
var resultRead = MutableLiveData<AppResponse>()
fun readNews(channel: String, guid: Int): Boolean{
GlobalScope.launch {
val response = apiService.readNewsAsync(Constants.API_TOKEN, channel, guid.toString()).await()
when{
response.isSuccessful -> {
val res = response.body()
val appRes = AppResponse(true, "ok", res!!)
resultRead.postValue(appRes)
}
else -> {
val appRes = AppResponse(false, "Error: ${response.message()}", null)
resultRead.postValue(appRes)
}
}
}
return true
}
}
Perhaps this activity is not getting stopped.
Check this out:
When you call readViewModel.getReadNews() in onCreate() your activity is created once, only if onStop is called will it be created again.
Hello iam learning build apps with kotlin but i got stack with this error says "Required Iterable, Found List", how i can solve this problem? please see my code below thanks
class MainActivity : AppCompatActivity(),ProductView {
private lateinit var productAdapter: ProductAdapter
private var productList: MutableList<ProductData> = mutableListOf()
private lateinit var dataPresenter : DataPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initRecycler();
getProduct()
}
private fun getProduct() {
dataPresenter = DataPresenter(applicationContext,this)
dataPresenter.getProduct()
}
private fun initRecycler() {
productAdapter = ProductAdapter(this,productList)
rvMain.layoutManager = LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false)
rvMain.adapter = productAdapter
}
override fun showLoading() {
pgMain.visibility = View.VISIBLE
}
override fun hideLoading() {
pgMain.visibility = View.GONE
}
override fun showProduct(products: List<ProductData>?) {
if (products?.size != 0){
this.productList.clear()
this.productList.addAll(products) // <= Required Iterable<ProductData>, Found List<ProductData>
productAdapter.notifyDataSetChanged()
}
}
}
I suspect that the error message actually is:
Required Iterable<ProductData>, Found List<ProductData>?
The question mark at the end is not just punctuation. That is the nullable indicator in Kotlin. A List<ProductData> cannot be null, but a List<ProductData>? can. And I believe that addAll() requires a non-null value.
Ideally, you should change ProductView so that the signature for showProduct() is fun showProduct(products: List<ProductData>).
Alternatively, you could rewrite showProduct() to be:
override fun showProduct(products: List<ProductData>?) {
if (products?.size != 0){
this.productList.clear()
products?.let { this.productList.addAll(it) }
productAdapter.notifyDataSetChanged()
}
}