Update:
I just tested my app on another device and found out that I do get the error on a Nexus 4 running Android 4.4.2, but NOT on an Desire S running Android 4.0.4. Both of them have the current YouTube App installed (5.3.32), which is required for using the API.
Question: Why do I get these ServiceConnectionLeaked messages? (See Logcat below)
Description:
I'm using the YouTube Android Player API 1.0.0 (https://developers.google.com/youtube/android/player/) to load video Thumbnails in a ListView with the following adapter code:
private final Map<View, YouTubeThumbnailLoader> mThumbnailViewToLoaderMap;
public ListViewAdapter(Activity activity, int layoutId, List<Suggestion> suggestions) {
super(activity, layoutId, suggestions);
mThumbnailViewToLoaderMap = new HashMap<View, YouTubeThumbnailLoader>();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
String videoId = getYoutubeId();
// There are three cases here...
if (convertView == null) {
convertView = LayoutInflater.from(mActivity).inflate(R.layout.row_suggestion, parent, false);
holder = new ViewHolder();
holder.thumbnailView = (YouTubeThumbnailView) convertView.findViewById(R.id.youtubeThumbnail);
// ... case 1: The youtube view has not yet been created - we need to initialize the YouTubeThumbnailView.
holder.thumbnailView.setLayoutParams(mThumbnailLayoutParams);
holder.thumbnailView.setTag(videoId);
holder.thumbnailView.initialize(DeveloperKey.DEVELOPER_KEY, this);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
// ... case 2 & 3 The view is already created and...
YouTubeThumbnailLoader loader = mThumbnailViewToLoaderMap.get(holder.thumbnailView);
// ... is currently being initialized. We store the current videoId in the tag.
if (loader == null) {
holder.thumbnailView.setTag(videoId);
// ... already initialized. Simply set the right videoId on the loader.
} else {
loader.setVideo(videoId);
}
}
holder.thumbnailView.setImageResource(R.drawable.thumbnail_loading);
return convertView;
}
@Override
public void onInitializationSuccess(YouTubeThumbnailView youTubeThumbnailView, YouTubeThumbnailLoader youTubeThumbnailLoader) {
String videoId = (String) youTubeThumbnailView.getTag();
mThumbnailViewToLoaderMap.put(youTubeThumbnailView, youTubeThumbnailLoader);
youTubeThumbnailLoader.setOnThumbnailLoadedListener(this);
youTubeThumbnailLoader.setVideo(videoId);
}
public void releaseLoaders() {
for (YouTubeThumbnailLoader loader : mThumbnailViewToLoaderMap.values()) {
loader.release();
}
}
The Fragment which holds this listView has the following code:
private ListViewAdapter listViewAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
rootView = (ViewGroup) inflater.inflate(R.layout.fragment_search_results, container, false);
listView = (ListView) rootView.findViewById(R.id.suggestion_list);
// see if there is already an adapter
// and use it as our adapter (e.g. after device rotation)
if (listViewAdapter != null) {
listView.setAdapter(listViewAdapter);
}
return rootView;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mListViewAdapter.releaseLoaders();
}
Here is what happens:
Now I rotate the device the first time (let's say into landscape orientation):
So this results in a list of thumbnails which alternate in being loaded correctly and not loaded at all. Now I rotate the device a second time (back to portrait orientation):
Now I rotate the device a third time (we are again in the previously erroneous landscape orientation):
So only after the first time the device gets rotated something happens, so visible thumbnails are not loaded into their list items.
The logcat shows the folllowing message for every thumbnail request after the first rotation:
03-09 17:43:08.770 30446-30446/com.mypackage.name E/ActivityThread: Activity com.mypackage.name.activities.SearchActivity has leaked ServiceConnection com.google.android.youtube.player.internal.r$e@41b0a6c0 that was originally bound here android.app.ServiceConnectionLeaked: Activity com.mypackage.name.activities.SearchActivity has leaked ServiceConnection com.google.android.youtube.player.internal.r$e@41b0a6c0 that was originally bound here at android.app.LoadedApk$ServiceDispatcher.(LoadedApk.java:1041) at android.app.LoadedApk.getServiceDispatcher(LoadedApk.java:935) at android.app.ContextImpl.bindServiceAsUser(ContextImpl.java:1692) at android.app.ContextImpl.bindService(ContextImpl.java:1680) at android.content.ContextWrapper.bindService(ContextWrapper.java:496) at com.google.android.youtube.player.internal.r.e(Unknown Source) at com.google.android.youtube.player.YouTubeThumbnailView.initialize(Unknown Source) at com.mypackage.name.adapters.ListViewAdapter.getView(ListViewAdapter.java:94) at com.mypackage.name.views.ListView.obtainView(ListView.java:1285) at com.mypackage.name.views.ListView.fillDown(ListView.java:1046) at com.mypackage.name.views.ListView.populate(ListView.java:720) at com.mypackage.name.views.ListView.onLayout(ListView.java:677) at android.view.View.layout(View.java:14520) at android.view.ViewGroup.layout(ViewGroup.java:4604) at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1077) at android.view.View.layout(View.java:14520) at android.view.ViewGroup.layout(ViewGroup.java:4604) at android.widget.FrameLayout.onLayout(FrameLayout.java:448) ...
Why do I get these ServiceConnectionLeaked messages?
I already searched around and in most cases there is some kind of API initialization necessary where one has to supply the application context instead of the activity context. But with the YouTubePlayer Android API there is no such initialization (at least not in my part of the code).
I finally found the solution. I traced every single step that was happening the first time all vies and fragments are created and compared it with what happens during the first rotation. Taking notes of every object created and re-used I found that during the onCreateView-Method of the Fragment I re-initiate the listView, but set the old adapter to it. This is not a problem so far and afaik good practice, since I can re-use every information the adapter holds to this point (correct me if I'm wrong).
The problem existed in the earlier initialization of the adapter. The adapter is initialized before the first rotation and got the current Activity as its Context. Therefore, after rotation, it still had the old portrait-activity as Context although it was lying around in a newly created landscape-activity. => classic activity leak.
It seems that the YoutubeThumbnail-initializer uses this context, although there is no hint or documentation on this. So when I initialize the adapter with an application context this app-context is used for the YouTubeThumbnails and no leak is produced.