I've created a simple app with one activity and I create a MediaSession in its onCreate method
However when I run the application and use external media buttons the callback is never called. Any ideas what I might be missing?
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
this.mediaSession = MediaSessionCompat(this, "TAG")
this.mediaSession?.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean {
return super.onMediaButtonEvent(mediaButtonIntent)
}
override fun onPlay() {
super.onPlay()
}
})
val builder = PlaybackStateCompat.Builder()
builder.setActions(PlaybackStateCompat.ACTION_PLAY)
builder.setState(PlaybackStateCompat.STATE_STOPPED, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 0.0f)
this.mediaSession?.setPlaybackState(builder.build())
this.mediaSession?.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
this.mediaSession?.isActive = true
}
You need to implement BroadcastReceiver. One example here: https://github.com/tutsplus/background-audio-in-android-with-mediasessioncompat/blob/master/app/src/main/java/com/tutsplus/backgroundaudio/BackgroundAudioService.java
Related
I am using lifecycleScope.launch in my activity's onCreate to collect a flow but I also am trying to attach a fragment inside the scope but I just get a black screen when trying to do this like it never gets attached.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
lifecycleScope.launch {
_viewModel.listenForScheduleChanges().flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {
}
_viewModel.loadConfig() // suspend method that loads information
_webFragment = WebFragment().apply {
arguments = Bundle().apply {
putParcelable("config", _viewModel.config)
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.contentPanel, _webFragment!!)
.commit()
}
}
If I dont use lifecycleScope and implement CoroutineScope in my activity with that coroutine scope the fragment attaches fine
class MainActivity : AppCompatActivity(), CoroutineScope{
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
lifecycleScope.launch {
_viewModel.listenForScheduleChanges().flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {
}
}
launch {
_viewModel.loadConfig() // suspend method that loads information
_webFragment = WebFragment().apply {
arguments = Bundle().apply {
putParcelable("config", _viewModel.config)
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.contentPanel, _webFragment!!)
.commit()
}
}
}
I dont understand why, both appear to be using the same context with the Dispatcher as Main.
Can someone provide insight here?
Separate the code that collects data from flow to another coroutine scope
your problem will be solved:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
_viewModel.listenForScheduleChanges().collect {
}
}
}
lifecycleScope.launch {
_viewModel.loadConfig() // suspend method that loads information
_webFragment = WebFragment().apply {
arguments = Bundle().apply {
putParcelable("config", _viewModel.config)
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.contentPanel, _webFragment!!)
.commit()
}
}
the issue is that when you collect a flow in this way it blocks the whole scope and other codes in the scope are not running.
I have a simple MainActivity and if the app is completely killed it looks like onCreate() is called once. If however I back out of the app so it still appears in the background, when I re-open it I get every log message twice. The weirdest part is if I generate a random number it is always the same in the 2 log messages.
I've tried adding android:LaunchMode="singleTop" (also singleInstance singleTask) in the activity and application tags of the Manifest.
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = binding.root
setContentView(view)
setupViews()
val data: Uri? = intent?.data
DataHolder.getInstance().setItem(data)
Timber.plant(Timber.DebugTree())
setupInjection()
Timber.d("review nanoTime = ${System.nanoTime()}")
Timber.d("review savedInstance = $savedInstanceState")
Timber.d("review random = ${Random.nextInt()}")
}
override fun onPause() {
Timber.d("review onPause()")
super.onPause()
}
override fun onStop() {
Timber.d("review onStop()")
super.onStop()
}
override fun onDestroy() {
Timber.d("review onDestroy()")
super.onDestroy()
finish()
}
override fun onStart() {
Timber.d("review onStart()")
super.onStart()
}
override fun onRestart() {
Timber.d("review onRestart()")
super.onRestart()
}
override fun onResume() {
Timber.d("review onResume()")
super.onResume()
}
private fun setupInjection() {
val appInjector = InjectorImpl(
firebaseAuth = FirebaseAuth.getInstance()
)
Injector.initialize(appInjector)
}
private fun setupViews() = binding.apply {
val navController = findNavController(R.id.nav_host_fragment)
navView.setupWithNavController(navController)
navView.setOnItemSelectedListener { item ->
when (item.itemId){
R.id.navigation_item_calculator -> {
navController.navigate(BuilderFragmentDirections.actionBuilderToCalculator())
}
R.id.navigation_item_builder -> {
navController.navigate(CalculatorFragmentDirections.actionCalculatorToBuilder())
}
}
true
}
navView.setOnItemReselectedListener { }
}
}
Here is a table of the log trace I get when I run the app on my phone from Android studio. Since the random numbers are the same I feel like this is actually a Logging bug in Android studio and the app isn't actually opened twice.
Realized my problem was with my logging library I used.
Timber was planting a new tree but wasn't uprooting old ones from being backed out so there were 2 instances of them. I fixed by putting a Timber.uprootAll() just before Timber.plant(Timber.DebugTree())
So I have my settings activity set up with my < PreferenceScreen > for choosing the app settings.
What I want to do is have my changes do immediate effect, and not on app restart.
Naturally I wanted to use OnSharedPreferenceChangeListener , but if I try to put a Log in onSharedPreferenceChanged, it never procs on changed selection. My plan was to recreate() on preference changed, so my preference apply function would proc on onCreate() with that recreate().
The problem is, as previously stated that onSharedPreferenceChanged never procs.
I tried registering the listener as it was stated somewhere, but it didn't help.
Can anyone help me?
class SettingsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
SharedPreferences.OnSharedPreferenceChangeListener {
val Tag = "My Activity:"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val prefMen = SharedPreferencesManager(this)
prefMen.loadTheme()
setContentView(R.layout.activity_settings)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.content_preference, MainPreference()).commit()
} else {
title = savedInstanceState.getCharSequence(TAG_TITLE)
}
supportFragmentManager.addOnBackStackChangedListener {
if (supportFragmentManager.backStackEntryCount == 0) {
setTitle("Settings")
}
}
setUpToolbar()
}
override fun onStart() {
super.onStart()
getPreferences(MODE_PRIVATE).unregisterOnSharedPreferenceChangeListener(this)
}
override fun onDestroy() {
super.onDestroy()
getPreferences(MODE_PRIVATE).unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
super.onSaveInstanceState(outState, outPersistentState)
outState.putCharSequence(TAG_TITLE, title)
}
override fun onSupportNavigateUp(): Boolean {
if (supportFragmentManager.popBackStackImmediate()) {
return true
}
return super.onSupportNavigateUp()
}
private fun setUpToolbar() {
supportActionBar?.setTitle("Settings")
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
}
class MainPreference : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
}
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat?,
pref: Preference?
): Boolean {
val args = pref?.extras
val fragment = pref?.fragment?.let {
supportFragmentManager.fragmentFactory.instantiate(
classLoader,
it
).apply {
arguments = args
setTargetFragment(caller, 0)
}
}
fragment?.let {
supportFragmentManager.beginTransaction().replace(R.id.content_preference, it)
.addToBackStack(null).commit()
}
title = pref?.title
return true
}
companion object {
private val TAG_TITLE = "PREFERENCE_ACTIVITY"
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
recreate()
}
}
I see two mistakes. The first is that you never register your listener. You have called unregisterOnSharedPreferenceChangeListener() in your onStart() instead of registerOnSharedPreferenceChangeListener().
Secondly, you are not listening to the same SharedPreferences as the settings Fragment is using. PreferenceFragmentCompat by default uses the default shared preferences of the whole application. But you are listening to preferences retrieved with Activity.getPreferences(), which are not the default preferences, but rather a SharedPreferences instance that is named after the Activity.
So you should switch to using default shared preferences, because the private shared preferences of the Activity will not be as easy to get a reference to in other activities.
I would also register in onCreate() instead of onStart() so you aren't registering multiple times. I don't know if that really matters, but the documentation doesn't specify what happens when you repeatedly register the same listener. Maybe it would fire the callback repeatedly, which could end up wasting your time hunting the bug later.
override fun onCreate() {
// ...
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(this)
}
override fun onDestroy() {
super.onDestroy()
PreferenceManager.getDefaultSharedPreferences(this)
.unregisterOnSharedPreferenceChangeListener(this)
}
It is possible to specify the SharedPreferences that will be used by PreferenceFragmentCompat, but the default preferences are the easiest to use for settings that you want to access from across your application.
I'm trying to implement OnBackPressedCallback
I followed the explanation here
https://developer.android.com/guide/navigation/navigation-custom-back
but it's incomplete
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
findNavController().navigate(R.id.action_showFragment_to_homeFragment)
}
//callback.handleOnBackPressed()
}
if i try to implement OnBackPressedCallback it's not ok
class ShowFragment : Fragment(), OnBackPressedCallback() {
Thank you
here is the solution
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
findNavController().navigate(R.id.action_showFragment_to_homeFragment)
}
requireActivity().onBackPressedDispatcher.addCallback(
this,
callback
)
None of the other instances of this question are solving my problem. I have a Fragment that appears at the end of a transaction sequence. It is meant to close the app when a CountDownTimer contained within it counts down:
class TerminalFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onStart() {
super.onStart()
startOverButton.setOnClickListener {
returnToStart()
}
initUi()
startCountDown()
}
override fun onStop() {
super.onStop()
AppLog.i(TAG, "onStop()")
stopCountdown()
}
}
private fun startCountDown() {
terminalCountdown = object : CountDownTimer(5000, 1000) {
override fun onFinish() {
AppLog.i(TAG, "Terminal countdown finished")
(context as MainActivity).finish()
}
override fun onTick(millisUntilFinished: Long) {
}
}
.start()
}
private fun stopCountdown() {
AppLog.i(TAG, "stopCountDown() - Terminal countdown stopped")
terminalCountdown?.cancel()
terminalCountdown = null
}
private fun returnToStart() {
AppLog.i(TAG, "returnToStart()")
stopCountdown()
(context as MainActivity).restartFlow()
}
stopCountDown() is being called whenever the fragment is navigated away from, but it somehow survives sometimes and closes the app from another Fragment. Using logs, I've also discovered that there appears to be 2 instances of this countdown sometimes. How do I insure that this countdown is never active outside of this fragment?
As Shrey Greg mentioned in a comment, I was creating a new instance of CountdownTimer every time I wanted to restart it. This lost the reference to the previous instance, making it impossible to cancel.