Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 038ad93e authored by George Mount's avatar George Mount
Browse files

Don't execute binding when the root view is detached.

Also add tests for memory leaks. The binder should be deleted
when the root view is deleted.

Change-Id: Ifcb24feb80791e64cdfd7203d071d9b1453f6f70
parent ff5868ed
Loading
Loading
Loading
Loading
+126 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 The Android Open Source Project
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *      http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.databinding.testapp;

import android.databinding.testapp.generated.LeakTestBinding;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import android.widget.FrameLayout;

import java.lang.ref.WeakReference;

public class LeakTest extends ActivityInstrumentationTestCase2<TestActivity> {
    WeakReference<LeakTestBinding> mWeakReference = new WeakReference<LeakTestBinding>(null);

    public LeakTest() {
        super(TestActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        try {
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    try {
                        LeakTestBinding binding = LeakTestBinding.inflate(getActivity());
                        getActivity().setContentView(binding.getRoot());
                        mWeakReference = new WeakReference<LeakTestBinding>(binding);
                        binding.setName("hello world");
                        binding.executePendingBindings();
                    } catch (Exception e) {
                        e.printStackTrace();
                        throw e;
                    }
                }
            });
            getInstrumentation().waitForIdleSync();
        } catch (Throwable t) {
            throw new Exception(t);
        }
    }

    public void testBindingLeak() throws Throwable {
        assertNotNull(mWeakReference.get());
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getActivity().setContentView(new FrameLayout(getActivity()));
            }
        });
        System.gc();
        assertNull(mWeakReference.get());
    }

    // Test to ensure that when the View is detached that it doesn't rebind
    // the dirty Views. The rebind should happen only after the root view is
    // reattached.
    public void testNoChangeWhenDetached() throws Throwable {
        final LeakTestBinding binding = mWeakReference.get();
        final AnimationWatcher watcher = new AnimationWatcher();

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getActivity().setContentView(new FrameLayout(getActivity()));
                binding.setName("goodbye world");
                binding.getRoot().postOnAnimation(watcher);
            }
        });

        watcher.waitForAnimationThread();

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertEquals("hello world", binding.getTextView().getText().toString());
                getActivity().setContentView(binding.getRoot());
                binding.getRoot().postOnAnimation(watcher);
            }
        });

        watcher.waitForAnimationThread();

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertEquals("goodbye world", binding.getTextView().getText().toString());
            }
        });
    }

    private static class AnimationWatcher implements Runnable {
        private boolean mWaiting = true;

        public void waitForAnimationThread() throws InterruptedException {
            synchronized (this) {
                while (mWaiting) {
                    this.wait();
                }
                mWaiting = true;
            }
        }


        @Override
        public void run() {
            synchronized (this) {
                mWaiting = false;
                this.notifyAll();
            }
        }
    }
}
+24 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2015 The Android Open Source Project
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <variable name="name" type="String"/>
    <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:text="@{name}"/>
</LinearLayout>
 No newline at end of file
+39 −2
Original line number Diff line number Diff line
@@ -16,10 +16,13 @@

package android.databinding;

import android.annotation.TargetApi;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;

import java.lang.ref.WeakReference;
@@ -71,6 +74,29 @@ public abstract class ViewDataBinding {
        }
    };

    private static final OnAttachStateChangeListener ROOT_REATTACHED_LISTENER;

    static {
        if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
            ROOT_REATTACHED_LISTENER = null;
        } else {
            ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
                @TargetApi(VERSION_CODES.KITKAT)
                @Override
                public void onViewAttachedToWindow(View v) {
                    // execute the pending bindings.
                    ViewDataBinding binding = (ViewDataBinding) v.getTag();
                    v.post(binding.mRebindRunnable);
                    v.removeOnAttachStateChangeListener(this);
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                }
            };
        }
    }

    /**
     * Runnable executed on animation heartbeat to rebind the dirty Views.
     */
@@ -78,10 +104,21 @@ public abstract class ViewDataBinding {
        @Override
        public void run() {
            if (mPendingRebind) {
                boolean rebind = true;
                if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
                    rebind = mRoot.isAttachedToWindow();
                    if (!rebind) {
                        // Don't execute the pending bindings until the View
                        // is attached again.
                        mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
                    }
                }
                if (rebind) {
                    mPendingRebind = false;
                    executePendingBindings();
                }
            }
        }
    };

    /**