I'm migrating from startActivityForResult to the Activity Result API. I have custom contract and a launcher:
private val gameContract=object: ActivityResultContract<Int, GameActivity.GameResult?>() {
override fun createIntent(context: Context, input: Int?): Intent {
return Intent(this#SelectorActivity, GameActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): GameActivity.GameResult? {
if (resultCode == Activity.RESULT_OK) {
val data = intent?.getStringExtra("data")
data?.let {
return Gson().fromJson(it, GameActivity.GameResult::class.java)
}
return null
}
return null
}
}
private val gameLauncher=registerForActivityResult(gameContract){
//
}
When I start activity with gameLauncher.launch(0) I got exception
java.lang.IllegalArgumentException: Can only use lower 16 bits for
requestCode
I tried different inputs with the same result
Apparently it expects requestCode to be set. But the API has no means to set it.
I thought the main purpose of this API is to get rid of messy request codes
So how do I start the activity with custom contract?
The problem was is that I forgot to add dependency
implementation 'androidx.fragment:fragment-ktx:1.3.2'
Related
I'm working on a library that has a couple of ready-made activities.
So far i have my activities in the library, and in the main app, i call it normally with registerForActivityResult to start it.
this means whoever is using my library would be able to see the whole activity.
what i would like to do, is to have the developer call a method in the library class and ask it to do an action, and in the library that method would on its own start the activity, register it for result, and return the result to the calling class through an interface.
the below is what i tried but it gives me error LifecycleOwner is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED
private fun launchScannerActivity(activity: FragmentActivity, callback: ScannerCallback) {
val scanResult =
activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
callback.onResult(it.data?.getStringExtra("Some Key") ?: "")
} else {
callback.onFail()
}
}
val intent = Intent(activity, ScannerActivity::class.java)
scanResult.launch(intent);
}
why do i need this:
This library would be an SDK for a SAAS product, so we would like to abstract and obfuscate as much of the implementation as possible from our clients.
You can't really communicate between Activities using interfaces, at least not in a way that is somewhat concise and isn't very prone to leaking. What you can do is expose your own Activity result contract. Then your API could be as simple as some of the ones in ActivityResultContracts. You can look at the source code there to see how to implement it.
Maybe something like this:
class ScannerResultContract : ActivityResultContract<Unit, String?>() {
override fun createIntent(context: Context, input: Unit?): Intent {
return Intent(context, ScannerActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): String? {
return if (resultCode == Activity.RESULT_OK) {
intent?.getStringExtra("Some Key")
} else {
null
}
}
}
Client usage:
// In activity or fragment:
val getScannerResult = registerForActivityResult(ScannerResultContract()) { resultString ->
if (resultString != null) {
// use it
} else {
// log no result returned
}
}
//elsewhere:
someListener.setOnClickListener {
getScannerResult.launch()
}
startActivityForResult(intent: Intent!, options: Bundle?) has been deprecated. I am trying to replace with ActivityResultLauncher but I need to pass the options. How can I do this with the new method? Below is an example of the original (now deprecated) method that would open the Contacts menu and then do one of two things in the switch based on the value of code:
...
val code = contactType //can be either 1 or 2
val contactsIntent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
contactsIntent.type = ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE
startActivityForResult(contactsIntent, code)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if(resultCode == Activity.RESULT_OK) {
when(requestCode) {
1 -> { //Do something
}
2 -> { //Do something else
}
}
}
}
I have tried to convert the above to use ActivityResultLauncher but I haven't figured out how to pass the value of code to it. Below is what I have so far:
val code = contactType //can be either 1 or 2
val contactsIntent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
contactsIntent.type = ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE
contactLauncher.launch(contactsIntent) //or maybe contactLauncher.launch(contactsIntent, code)?
private val contactLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if(it.resultCode == Activity.RESULT_OK) {
when(??? requestCode ???) {
1 -> { //Do something
}
2 -> { //Do something else
}
}
}
}
In this situation, you'll need to create two separate ActivityResultLauncher objects, one for each case.
IMO, this is exactly what Google was trying to solve, having a cluttered "onActivityResult" function, as well as having to deal with requestCodes. Right now, this is more similar to an OnClickListener kind of callback.
They did the same with other parts of Android, like requesting app permissions. The requestCode is now handled internally.
An overloaded version of 'launch()' allows you to pass an 'ActivityOptionsCompat' in addition to the input.
#see:
https://developer.android.com/reference/androidx/activity/result/ActivityResultLauncher
I am trying to capture an image from the camera and display it on the imageView. I tried but getting the error "override method should call super.onActivityResult", you can see my code below. Please let me know if I am doing it right.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == TAKE_PICTURE && resultCode == Activity.RESULT_OK) {
try{
val file= File(currentPath)
val uri = Uri.fromFile(file)
val imageView = findViewById<ImageView>(R.id.imageView)
imageView.setImageURI(uri)
}catch(e:IOException){
e.printStackTrace()
}
}
if (requestCode == PICK_PICTURE && resultCode == Activity.RESULT_OK) {
try{
val uri = data!!.data
val imageView = findViewById<ImageView>(R.id.imageView)
imageView.setImageURI(uri)
}catch(e:IOException){
e.printStackTrace()
}
}
}
add super.onActivityResult(requestCode, resultCode, data); as first line on the onActivityResult method.
What does it mean?
When a class extends another, say you have class A and class B : A
In this context, class A is the super class of B.
So if class A has a method called:
fun someMethod()
you can do:
var myB = B()
myB.someMethod()
this effectively calls the code in A, because B extends A, and someMethod is not private.
Now if you want to modify someMethod's behavior in B, you override it...
override fun someMethod() {
// b does something different here
}
now, you can mark A's someMethod as #CallSuper (an annotation coming from here.
In which case you get the warning/error that B must call its super (know as Parent too) class too...
so B must now do:
override fun someMethod() {
super.someMethod()
// your B code too..
}
There's no rule to call it at the beginning you can call it at any time, as long as you do before the end of the function. In some instances, it's desired to call it as the first thing (if, for instance, A does something you need in B as well), and in some other cases, you want to wait for B to do something before calling super.... As long as you do.
onActivityResult is marked as such, and therefore you must call super.
I have a thirdparty Android control that can't be used in Flutter directly. I put it in an Android activity. Then, using information from https://flutter.io/docs/development/platform-integration/platform-channels#step-3b-add-an-android-platform-specific-implementation-using-kotlin, I can successfully launch that activity and perform some actions there. The only part that doesn't work is sending results back from the activity.
Flutter code:
void showDialog() async
{
try {
final Map<String, List<double>> result = await platform.invokeMethod('show_dialog',
<String, String>{
'address': widget.user.address
});
widget.user.address = result.keys.toList()[0];
} on PlatformException catch (e) {
print('Failed to pick address: ${e.message}.');
}
}
Android code:
class MainActivity: FlutterActivity() {
private val CHANNEL = "dialog"
private lateinit var _result: MethodChannel.Result
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "show_dialog") {
_result = result
val intent = Intent(this#MainActivity, DialogActivity::class.java)
intent.putExtra("address", call.argument<String>("address"))
startActivityForResult(intent, 9689)
}
else result.notImplemented()
}
}
override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) {
if(requestCode != 9689)
return super.onActivityResult(requestCode, result, intent)
if (result == Activity.RESULT_OK) {
_result.success(mapOf(intent!!.getStringExtra("address") to
listOf(intent.getDoubleExtra("latitude", 0.0),
intent.getDoubleExtra("longitude", 0.0))))
}
else
_result.success(null)
}
}
What's the problem? Breakpoint on the line widget.user.address = result.keys.toList()[0]; is never reached, suggesting the result is never sent back.
Turns out the code was almost correct. The Android side didn't need any changes, but on the Flutter side I had to make this change:
turn
final Map<String, List<double>> result = await platform.invokeMethod(
into
final result = await platform.invokeMethod(
i. e. simply remove explicit type from the variable, because the return value of platform.invokeMethod was some kind of an internal hash map (in particular, it's name started with an underscore) rather than that of Map as specified. Flutter didn't show any errors in the console output, because for some reason it only captures Android log with debugger attached to the Android part of the application, and once I figured how to debug Android code in Android Studio, I immediately found the reason.
TL;DR: I have successfully created and coupled (via a subscription) an activity to a media browser service. This media browser service can continue running and play music in the background. I'd like to be able to refresh the content at some stage, either when the app comes to the foreground again or during a SwipeRefreshLayout event.
I have the following functionality I'd like to implement:
Start a MediaBrowserServiceCompat service.
From an activity, connect to and subscribe to the media browser service.
Allow the service to continue running and playing music while the app is closed.
At a later stage, or on a SwipeRefreshLayout event, reconnect and subscribe to the service to get fresh content.
The issue I am receiving is that within a MediaBrowserService (after a subscription has been created) you can only call sendResult() once from the onLoadChildren() method, so the next time you try to subscribe to the media browser service using the same root, you get the following exception when sendResult() is called for the second time:
E/UncaughtException: java.lang.IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: MEDIA_ID_ROOT
at android.support.v4.media.MediaBrowserServiceCompat$Result.sendResult(MediaBrowserServiceCompat.java:602)
at com.roostermornings.android.service.MediaService.loadChildrenImpl(MediaService.kt:422)
at com.roostermornings.android.service.MediaService.access$loadChildrenImpl(MediaService.kt:50)
at com.roostermornings.android.service.MediaService$onLoadChildren$1$onSyncFinished$playerEventListener$1.onPlayerStateChanged(MediaService.kt:376)
at com.google.android.exoplayer2.ExoPlayerImpl.handleEvent(ExoPlayerImpl.java:422)
at com.google.android.exoplayer2.ExoPlayerImpl$1.handleMessage(ExoPlayerImpl.java:103)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:150)
at android.app.ActivityThread.main(ActivityThread.java:5665)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:822)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:712)
I call the following methods to connect to and disconnect from the media browser (again, everything runs smoothly on first connection, but on the second connection I'm not sure how to refresh the content via a subscription):
override fun onStart() {
super.onStart()
mMediaBrowser = MediaBrowserCompat(this, ComponentName(this, MediaService::class.java), connectionCallback, null)
if (!mMediaBrowser.isConnected)
mMediaBrowser.connect()
}
override fun onPause() {
super.onPause()
//Unsubscribe and unregister MediaControllerCompat callbacks
MediaControllerCompat.getMediaController(this#DiscoverFragmentActivity)?.unregisterCallback(mediaControllerCallback)
if (mMediaBrowser.isConnected) {
mMediaBrowser.unsubscribe(mMediaBrowser.root, subscriptionCallback)
mMediaBrowser.disconnect()
}
}
I unsubscribe and disconnect in onPause() instead of onDestroy() so that the subscription is recreated even if the activity is kept on the back-stack.
Actual method used for swipe refresh, in activity and service respectively:
Activity
if (mMediaBrowser.isConnected)
mMediaController?.sendCommand(MediaService.Companion.CustomCommand.REFRESH.toString(), null, null)
Service
inner class MediaPlaybackPreparer : MediaSessionConnector.PlaybackPreparer {
...
override fun onCommand(command: String?, extras: Bundle?, cb: ResultReceiver?) {
when(command) {
// Refresh media browser content and send result to subscribers
CustomCommand.REFRESH.toString() -> {
notifyChildrenChanged(MEDIA_ID_ROOT)
}
}
}}
Other research:
I have referred to the Google Samples code on Github, as well as...
https://github.com/googlesamples/android-MediaBrowserService
https://github.com/moondroid/UniversalMusicPlayer
Neither of the above repos seem to handle the issue of refreshing content after the media browser service has been created and the activity has subscribed at least once - I'd like to avoid restarting the service so that the music can continue playing in the background.
Possible related issues:
MediaBrowser.subscribe doesn't work after I get back to activity 1 from activity 2 (6.0.1 Android) --no effect on current issue
Calling you music service implementations notifyChildrenChanged(String parentId) will trigger the onLoadChildren and inside there, you can send a different result with result.sendResult().
What I did was that I added a BroadcastReceiver to my music service and inside it, I just called the notifyChildrenChanged(String parentId). And inside my Activity, I sent a broadcast when I changed the music list.
Optional (not Recommended) Quick fix
MusicService ->
companion object {
var musicServiceInstance:MusicService?=null
}
override fun onCreate() {
super.onCreate()
musicServiceInstance=this
}
//api call
fun fetchSongs(params:Int){
serviceScope.launch {
firebaseMusicSource.fetchMediaData(params)
//Edit Data or Change Data
notifyChildrenChanged(MEDIA_ROOT_ID)
}
}
ViewModel ->
fun fetchSongs(){
MusicService.musicServiceInstance?.let{
it.fetchSongs(params)
}
}
Optional (Recommended)
MusicPlaybackPreparer
class MusicPlaybackPreparer (
private val firebaseMusicSource: FirebaseMusicSource,
private val serviceScope: CoroutineScope,
private val exoPlayer: SimpleExoPlayer,
private val playerPrepared: (MediaMetadataCompat?) -> Unit
) : MediaSessionConnector.PlaybackPreparer {
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?
): Boolean {
when(command){
//edit data or fetch more data from api
"Add Songs"->{
serviceScope.launch {
firebaseMusicSource.fetchMediaData()
}
}
}
return false
}
override fun getSupportedPrepareActions(): Long {
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
}
override fun onPrepare(playWhenReady: Boolean) = Unit
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
firebaseMusicSource.whenReady {
val itemToPlay = firebaseMusicSource.songs.find { mediaId == it.description.mediaId }
playerPrepared(itemToPlay)
}
}
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) = Unit
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) = Unit
}
MusicServiceConnection
fun sendCommand(command: String, parameters: Bundle?) =
sendCommand(command, parameters) { _, _ -> }
private fun sendCommand(
command: String,
parameters: Bundle?,
resultCallback: ((Int, Bundle?) -> Unit)
) = if (mediaBrowser.isConnected) {
mediaController.sendCommand(command, parameters, object : ResultReceiver(Handler()) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
resultCallback(resultCode, resultData)
}
})
true
} else {
false
}
ViewModel
fun fetchSongs(){
val args = Bundle()
args.putInt("nRecNo", 2)
musicServiceConnection.sendCommand("Add Songs", args )
}
MusicService ->
override fun onLoadChildren(
parentId: String,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
when(parentId) {
MEDIA_ROOT_ID -> {
val resultsSent = firebaseMusicSource.whenReady { isInitialized ->
if(isInitialized) {
try {
result.sendResult(firebaseMusicSource.asMediaItems())
if(!isPlayerInitialized && firebaseMusicSource.songs.isNotEmpty()) {
preparePlayer(firebaseMusicSource.songs, firebaseMusicSource.songs[0], true)
isPlayerInitialized = true
}
}
catch (exception: Exception){
// not recommend to notify here , instead notify when you
// change existing list in MusicPlaybackPreparer onCommand()
notifyChildrenChanged(MEDIA_ROOT_ID)
}
} else {
result.sendResult(null)
}
}
if(!resultsSent) {
result.detach()
}
}
}
}
My issue was unrelated to the MediaBrowserServiceCompat class. The issue was coming about because I was calling result.detach() in order to implement some asynchronous data fetching, and the listener I was using had both the parentId and result variables from the onLoadChildren method passed in and assigned final val rather than var.
I still don't fully understand why this occurs, whether it's an underlying result of using a Player.EventListener within another asynchronous network call listener, but the solution was to create and assign a variable (and perhaps someone else can explain this phenomenon):
// Create variable
var currentResult: Result<List<MediaBrowserCompat.MediaItem>>? = null
override fun onLoadChildren(parentId: String, result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>) {
// Use result.detach to allow calling result.sendResult from another thread
result.detach()
// Assign returned result to temporary variable
currentResult = result
currentParentId = parentId
// Create listener for network call
ChannelManager.onFlagChannelManagerDataListener = object : ChannelManager.Companion.OnFlagChannelManagerDataListener {
override fun onSyncFinished() {
// Create a listener to determine when player is prepared
val playerEventListener = object : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
when(playbackState) {
Player.STATE_READY -> {
if(mPlayerPreparing) {
// Prepare content to send to subscribed content
loadChildrenImpl(currentParentId, currentResult as MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>)
mPlayerPreparing = false
}
}
...
}
}
}
}