Bug 26708 - FragmentManager.FindFragmentByTag failing Re support Ticket number 112108
Summary: FragmentManager.FindFragmentByTag failing Re support Ticket number 112108
Status: RESOLVED ANSWERED
Alias: None
Product: Android
Classification: Xamarin
Component: General ()
Version: 5.1
Hardware: PC Windows
: --- normal
Target Milestone: ---
Assignee: Jonathan Pryor
URL:
Depends on:
Blocks:
 
Reported: 2015-02-03 21:25 UTC by Graham McKechnie
Modified: 2015-04-01 17:04 UTC (History)
2 users (show)

Tags:
Is this bug a regression?: ---
Last known good build:

Notice (2018-05-24): bugzilla.xamarin.com is now in read-only mode.

Please join us on Visual Studio Developer Community and in the Xamarin and Mono organizations on GitHub to continue tracking issues. Bugzilla will remain available for reference in read-only mode. We will continue to work on open Bugzilla bugs, copy them to the new locations as needed for follow-up, and add the new items under Related Links.

Our sincere thanks to everyone who has contributed on this bug tracker over the years. Thanks also for your understanding as we make these adjustments and improvements for the future.


Please create a new report on Developer Community or GitHub with your current version information, steps to reproduce, and relevant error messages or log files if you are hitting an issue that looks similar to this resolved bug and you do not yet see a matching new report.

Related Links:
Status:
RESOLVED ANSWERED

Description Graham McKechnie 2015-02-03 21:25:48 UTC
Hi Android Team,

I'm developing an OBDII app based on my existing Windows OBDII program.

The problem I've come across, first reported to support Dec 18, is that FragmentMangager.FindFragmentByTag is returning null when it should be returning the fragment that is being viewed on the screen. I was able to trace the problem to the test for a null fragment. 

A little background about the app may help. Please jump to the paragraph starting "I sent the app to support Jan7th" if background not required

An OBDII application connects to any OBDII compliant vehicle via an OBDII scan tool. Once the scan tool is connected to the vehicle, via the OBDII connector inside the vehicle, it can read the various ECU(s) of a vehicle and the app can report values from the various Parameter Identifiers (Pids). The app links in my Windows OBDII dll which is essentially the original Windows code with only a slight modification in the way that the Bluetooth connection is made, because of the fact that Windows (.Net 2.0) doesn't directly support connecting via the Bluetooth device address as Android does. The remainder of the OBDII code is unchanged and has been in production since 2007.

The app basically consists of a single Activity and a number of fragments. Which fragment is displayed is determined (by the user) by choosing a menu option from a NavigationDrawer. The fragment tag names in use at this stage of the development are Connection, ConnectionSummary, SupportedPids and ReadVehiclePids. The Connection fragment just displays a progress dialog underlayed by a listview which shows various responses from the ECUs as the connection is established and the various OBDII services that the vehicle supports are reported. At the completion of the initialization process the user can choose any one of the other 3 fragments to view. 1 the static data of the ConnectionSummary fragment, 2 the static data of the SupportedPids fragment which displays a list of all the Pids that this particular vehicle supports and 3 the ReadVehiclePids fragment which allows the user to then read the data of those pids in real time. All of these fragments work fine in both portrait and landscape mode. The ReadVehiclePids fragment, differs in that it uses different layouts for portrait and landscape. The landscape layout displays extra information such as Minimum, Maximum and Average values, where as the Portrait layout is restricted to  only values and their display units either metric or imperial.

The OBDInterpreter runs as a service, so that data can still be logged to file even when the app is in the background. The service is only terminated when the user exits the program (back button Double tap). It is a user choice whether data is continuously read, controlled by two menu choices on the ActionBar  Read and Cancel Read.

By default once the initialization is complete the user can optionally bypass both the ConnectionSummary fragment and the SupportedPids fragment and choose the ReadVehiclePids fragment and then begin reading the real time data. That would be the usual choice of most first time users. In that case they are then reading in a loop in sequential order the values of all the pids that the vehicle supports. There are approx 130 pids supported by the OBDII specification (SAE 1979 or ISO 15031-5). However most vehicles only supported a subset of those 130. Each manufacturer is free to support whatever pids they determine that is needed for a particular vehicle. Older vehicles may only support approximately 20-25 pids, whereas late model vehicles utilizing revised versions of the specification are more likely to support 45-70 pids. Our OBDII dll is event driven, therefore we have an event for each of those pids. If the vehicle supports a particular pid then that event fires and is received on the MainActivity. Obviously if the vehicle doesn't support a particular pid, then that event for that particular pid will never fire. Consequently we have in the MainActivity code such as the following example for each pid.

#region 0x05 EngineTemperatureSensorReceived
void obdInterpreter_EngineTemperatureSensorReceived(object sender, EngineTemperatureSensorReceivedEventArgs e)
{
      string max = EngineTemperatureSensorReceivedEventArgs.Maximum.ToString();
      string min = EngineTemperatureSensorReceivedEventArgs.Minimum.ToString();
      string average = EngineTemperatureSensorReceivedEventArgs.Average.ToString();

      SelectedPidItem selectedPidItem = null;

      ReadVehiclePidsFragment readVehiclePidsFragment = (ReadVehiclePidsFragment)FragmentManager.FindFragmentByTag("ReadVehiclePids");
      if (readVehiclePidsFragment != null)
      {
            selectedPidItem = new SelectedPidItem("0" + e.Sensor.ToString(), e.SensorDescription, e.Units, e.Degrees, max, min,average);
            readVehiclePidsFragment.UpdateAdapter(selectedPidItem);
      }
      else
      {
            // How can it be null if this is the fragment now displayed on the screen.
            Log.Debug(logTag, "Can't update Calculated Load - ReadVehiclePids object is somehow null");
      }
}
#endregion

As the program is further developed, the above piece of code will expand to cater for different fragments. As an example when we add gauges to the app. Not all pids have data that is suitable to display on an analogue gauge, therefore only some events will have additional tests such as 

if (gaugeFragment != null)
{
      UpdateGaugeValue( e.Whatever) 
}

All the above works well in both portrait and landscape modes, mimicking exactly what the Windows program does. Devices that have been tested are HTC Sensation Android 4.0.3, Nexus 4, 7, 10 Android 4.4.4. The Nexus 4, 7 and 10 have since been upgraded to 5.0.1, 5.0.2 and 5.0.1 respectively with no change in behaviour.

One of features that is essential is to allow the user to select their own selection of pids. Therefore the SupportedPids fragment allows the user to deselect all the supported pids a particular vehicle supports and then individually select pids they wish to read. That is common scenario where a technician may for instance only want to view the Oxygen Sensors Pids to determine which particular O2 sensor is faulty. Once the selection is made, the user can then reselect the ReadVehiclePids fragment and read data from just the pids they have selected. This also has the advantage of updating the information faster as only a smaller number of events will fire through the loop. Again this works fine in both portrait and landscape mode. The pid rate indicator at the bottom of the screen shows a greater output rate, so the user soon learns that the real time information can be displayed faster if they choose a smaller subset of pids to view. However we also want the user to be able to quickly revert back to the full list of pids, so the SupportedPidsFragment has an additional menu choice that allows the user to revert back to the full original list again. From there they can accept the full list or again make a different subset of pids to view.

This is where the problem crops up. As long as the screen is not rotated we can swap between all selected, to a partial selection and swap to the ReadVehiclePids fragment and read the values. You can swap back and forth as many times as you want between the full list of pids and a partial list of pids  and it reads the values selected perfectly. However once the screen has been rotated, when you selected either a partial list or the full list when you then come back to read the values, the pid information displays correctly, but the values never update. It is quite obvious that the scan tool is reading the values because the Tx,Rx lights on the scan tool are still flashing like a Christmas tree as usual, but there are no values being updated on the screen and the pid rate meter is reading zero. After locating the problem with Log.Debug() I then set a break point in the debugger on the Log.Debug line above and sure enough it is only hit ( readVehiclePidsFragment is null ) after the screen has been rotated and then a new selection is made (either full to partial or partial to full).


There was a considerable delay in hearing back from support because of the holiday period. I then wrote again to support Jan 5th, with the following comments.

Jan 5th
In the interim I've done more debugging and need to correct an assumption I made in my original post. I originally stated that the problem only occurred after the SupportedPidsFragment had been rotated and the user then went back to the ReadVehiclePidsFragment to continuously read data from the ECU(s). See the paragraph in the original email below starting "This is where the problem crops up".

I've now found that the problem will occur if any of the other 3 fragments ConnectionFragment, ConnectionSummaryFragment or SupportedPidsFragment are rotated prior to attempting to Read Pids on the ReadVehiclePidsFragment. If the screen isn't rotated, then the problem doesn't occur. The app can be started in either Portrait or Landscape mode and behave perfectly. When the ReadVehiclePidFragment is reading values, the screen can be rotated back and forth and the app operates flawlessly. ReadVehiclePidsFragment is the only fragment that has different layouts for portrait and landscape. As stated below the user can swap out to the SupportedPidsFragment change the list of Pids to be selected and swap back to the ReadVehiclePidsFragment and the data for each selected pid is updated in real time as expected. The only proviso is  don't swap out to another fragment and then rotate that fragment because then when you return to the ReadVehiclePidsFragment the values of the selected pids no longer update due to that fact that FragmentManager.FindFragmentByTag returns null and therefore the code to update the adapter never executes.

I also original stated that in the MainActivity method OBDStartContinuousRead that a test for FragmentManager.FindFragmentByTag("ReadVehiclePids") returned the fragment and the time between the fragment getting lost was 0.020 secs when the event executed as per the logcat. I also found the OBDStopRead method, executed when the user clicks CancelRead on the ReadVehiclePidsFragment,  also reports that the fragment is found. So the only time the fragment is lost, is in each event as per in the original example obdInterpreter_EngineTemperatureSensorReceived.

Because I couldn't come up with any logical explanation, I introduced a new ivar of MainActivity (for debugging purposes) called currentFragment declared as Fragment. I was hoping instead of using FragmentManager.FindFragmentByTag that I could cast the currentFragment back to a ReadVehiclesPidsFragment and then execute readVehiclePidsFragment.UpdateAdapter(selectedPidItem). I also had to add another string ivar currentFragmentTag as I immediately had trouble with currentFragment.Tag being null. I then added a StateHolderFragment - a no UI fragment or headless fragment to save/restore in MainActivity's OnPause/OnRestore the value of currentFragment and currentFragmentTag. They are initialized as follows each time a new fragment is selected.

fragmentTransaction.Replace(Resource.Id.content_frame, fragment, tag);
fragmentTransaction.Commit();   
currentFragment = fragment;
currentFragmentTag = tag;

The following is the output from Log.Debug inside the event when you have not rotated the screen previously when the update adapter code executes

currentFragment is ReadVehiclePidsFragment{2332d658 #1 id=0x7f0a001c ReadVehiclePids}
currentFragmentTag is ReadVehiclePids
. No the ToString representation shows an ID and a Tag. The results are as expected and of course the code executes and the adapter is updated.

The following is the output from Log.Debug inside the event when you have rotated the screen previously. For example  when on the ConnectionSummaryFragment prior to selecting the ReadVehiclePidsFragment. This is just an example, it can be repeated with any of the other 3 fragments. Just substitute the fragment names.

Can't update Calculated Load - ReadVehiclePids object is somehow null
currentFragment is ConnectionSummaryFragment{c475866}
currentFragment.Tag is IsNullOrEmpty
currentFragmentTag is ConnectionSummary 

Why the currentFragment is the ConnectionSummary (or any of the other two) when we have just selected the ReadVehiclePidsFragment and assigned the values to currentFragment etc and are presently viewing the ReadVehiclePidsFragment has really got me stumped. However, I hope one of you guys can explain and suggest either a work around or agree that it is a bug in FragmentManager.

Note how the ToString() on the ConnectionSummary fragment is in quite a different form as compared to the ToString() of ReadVehiclePids fragment. It appears as though the fragment is incomplete because the Fragment.Tag always comes back as null when you check it in the debugger, but obviously the string currentFragmentTag was somehow saved correctly, albeit with the wrong value. It looks also like the fragment has lost its Id.

I sent the app to support Jan7th and after much "toing and froing" it became obvious to me, since your support staff couldn't run the app - no scan tool, it would be easier if I came up with an app which displayed the same behaviour without the need of scan tool. I choose a GPS app, using an external Bluetooth GPS because NMEA Bluetooth GPS receivers are readily available and I already had an AndroidGPS.dll which is very similar in design to my OBDIIAndroid.dll. The code of the GPSReceiver is almost identical to OBDReceiver. The only difference is that the GPSReceiver doesn't write to the device, but it uses virtually the same code to read the gps sentences as the OBDReceiver does to read the obd responses. The GPSInterpreter class posts the various gps sentences back to the MainActivity in exactly the same way as the OBDInterpreter class. On Jan 19th I sent the GPS app to support. It demonstrated exactly the same problem of FindFragmentByTag failing. Unfortunately I still wasn't able to convince Jon Goldberger in support - please see support Ticket 112108 for the details . I've now added a further fragment which contains a listview, which mimics the behaviour of the ReadVehiclePids fragment in the OBD app. It also uses two different layouts to further mimic the ReadVehiclePids fragment of the OBD app. Please note that the landscape view is very much contrived, just to mimic the behaviour of the ReadVehiclePids fragment. The GPS app is also using Android.Support.V4.App.FragmentManager, which was just an exercise to see if there was any different behaviour and to be able to use FragmentManager.Fragments (for debugging purposes) which is not available in the later version. Since my OBDII app is only intended to support API 15 and above, Android.Support.V4.App.FragmentManager is not really required. I'm now still not sure if support (Jon) agrees that this is a bug, but evidently I have exhausted their ability to provide further support and they have requested that I file this issue as a bug.

The GPS app consists of 3 fragments, a PositionFragment, GPSSentence fragment and a BluetoothProperties fragment. The Position and the GPSSentence fragments contains the output from the GPS events. The BluetoothProperties fragment, mimicking the ConnectionSummary fragment of the OBDII app, shows the connected Bluetooth Device Name and Bluetooth Address which are properties of the GPSInterpreter class.

In the final modification of the GPS app I was able to come up with a partial workaround. Previously I subscribed to the gpsInterpreter events once in the method InitialiseInterpreter of the MainActivity. I thought about unsubscribing to those events for any fragment(s) that don't require them, e.g. the BluetoothProperties fragment and then resubscribe for the fragments that do require them. So in the OnPause of the Position fragment and the GPSSentence fragment I now call a method that will unsubscribe the events (-=) and then resubscribe(+=) the events in the OnResume of those fragments. This appears to fix the problem of FindFragmentByTag failing on the fragment that the user is viewing. I can't explain why, but it is very easy to demonstrate just by commenting out the code in the OnPause and OnResume of those two fragments. 

However it is only a partial solution for in the OBDII app, the OBD app is required to log the OBDII events to a log file, when the app is in the background, so the present solution will not enable that feature, since when the app is in the background it is now not receiving the events. The partial workaround sort of defeats the purpose of running the GPSInterpreter as a service, but at least it demonstrates the problem of FindFragmentByTag failing under certain circumstances.

Some notes:

I am using Android Studio's ADB logs as it allows a regex expression for a log tag. I have not been able to find a way to add an expression such as GLM* to filter logcat output when using the Xamarin version in Visual Studio, under Android Device Logging.

The GPS app is cosmetically a bit rough on the edges and has not been tested on a phone, but is acceptable on a tablet such as Nexus 7.

The two events in the MainActivity that have Log.Debug info are TimeRecieved and GPSSentenceReceived. The TimeReceived is only sent to the Position fragment, where as the GPSSentenceReceived event is sent to both the Position fragment and the GPSSentence fragment

Steps to reproduce the problem.
As uploaded the project is in the partial workaround state. All screens appears to work ok. 

To reproduce the problem. 
In both the Position fragment and the GPSSentence fragment comment UnsubscribeToInterpreterEvents and SubsubscribeToInterpreterEvents in OnPause and OnResume respectively and redeploy.

Select Position (the default) from the menu. Then tap Connect. Once the Position Fragment is receiving GPS events, Select Bluetooth GPS Properties. The device name and bluetooth address will be displayed. Rotate the screen and observe the same properties. Now reselect the Position fragment and there will be no output on the Position fragment. The output from the Log.Debug statements will be as follows

02-04 10:02:41.625    3952-3952/LocationNmea.LocationNmea D/GLM - MainActivity﹕ GPSSentenceReceived Event - Can't find PositionFragment. SupportFragmentManager.Fragments[0] is BluetoothPropertiesFragment{7639d42} Fragment Count is 1
02-04 10:02:41.625    3952-3952/LocationNmea.LocationNmea D/GLM - MainActivity﹕ GPSSentenceReceived Event - Can't find GPSSentenceFragment. SupportFragmentManager.Fragments[0] is BluetoothPropertiesFragment{7639d42} Fragment Count is 1

Notice how SupportFragmentManager.Fragments.Count of 1 and that the only element is showing the BluetoothPropertiesFragment without an ID or Tag. It doesn't matter which fragment, but it will always be the previously rotated fragment. There is no mention of the PositionFragment even though we are viewing it. Compare that to the earlier entries when the GPS data is first displayed of the Position fragment where all the logcat data is correct.

My conclusion is that somewhere inside FragmentManager, that either the two arraylists mAdded  or mActive get out of kilter.

I look forward to your comments.

Regards
Graham McKechnie
Comment 2 Brendan Zagaeski (Xamarin Team, assistant) 2015-04-01 17:04:14 UTC
To do a little bit of tidying-up, I'll mark this bug as "resolved answered," and add a few notes.


After some further investigation over email, it seems the key clue was:

> Notice how SupportFragmentManager.Fragments.Count of 1 and that the only
> element is showing the BluetoothPropertiesFragment without an ID or Tag


This led to documentation about the tricky interactions between EventHandlers and Activities:
> [1] http://developer.xamarin.com/guides/cross-platform/deployment%2c_testing%2c_and_metrics/memory_perf_best_practices/#Removing_Event_Handlers_in_Activities



Sure enough, it looks like the curious behavior where the BluetoothPropertiesFragment loses its ID and Tag are a result of the C# FragmentManager staying alive while the underlying Java FragmentManager is destroyed. After the device rotation, the event handler is still trying to access fragments using the old Java FragmentManager (owned by the old, destroyed Activity [2]), and that causes confusing results. Unsubscribing and resubscribing the event handlers stops the problem because it updates the event handlers to access fragments via the _new_ FragmentManager, owned by the new Activity.


> [2] "A configuration change (such as a change in screen orientation,
> language, input devices, etc) will cause your current activity to be
> destroyed".
> (http://developer.android.com/reference/android/app/Activity.html)



One possible way to avoid these tricky interactions between EventHandlers and Activities could be to move the logging into its own Android Service [3].

> [3] "Sometimes an Activity may need to do a long-running operation that
> exists independently of the activity lifecycle itself... To accomplish this,
> your Activity should start a Service"
> (http://developer.android.com/reference/android/app/Activity.html)