In ListView: Activity has leaked ServiceConnection <youtube.player.internal> that was originally bound here

muetzenflo picture muetzenflo · Mar 9, 2014 · Viewed 10.1k times · Source

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:

  • The Fragment is created, the YouTube-Thumbnails are loaded and displayed correctly.

Now I rotate the device the first time (let's say into landscape orientation):

  • The Fragment is created, the already existing adapter is connected to the listView, but no thumbnail is loaded for all currently visible list items.
  • When I scroll down, new list items enter the screen.
    • If such a new list items uses a new ViewHolder its thumbnail is loaded correctly.
    • If such a new list items uses a ViewHolder from the first visible list items its thumbnail is not loaded at all.

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):

  • The Fragment is created, the existing adapter is connected to the listView and all thumbnails are loaded correctly.

Now I rotate the device a third time (we are again in the previously erroneous landscape orientation):

  • The Fragment is created, the existing adapter is connected to the listView and all thumbnails are loaded correctly.

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).

Answer

muetzenflo picture muetzenflo · Mar 13, 2014

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.