Fix memory leak in NotificationAdapter

OnConfigurationChange would create a new view and a new adapter, but the
adapter is registered in the PreprocessingManager and will never be
garbage collected. By unregistering the adapter from the recyclerview
and the PreprocessingManager this leak can be prevented.

Bug: 301492797
Test: Manual (memory dump)
Test: atest CarNotificationUnitTests
Change-Id: I4baf7ef2f9ecfc01de814a7a52ce49f1ef6838c4
diff --git a/src/com/android/car/notification/CarNotificationView.java b/src/com/android/car/notification/CarNotificationView.java
index 8a9ccd7..5dd43f8 100644
--- a/src/com/android/car/notification/CarNotificationView.java
+++ b/src/com/android/car/notification/CarNotificationView.java
@@ -54,7 +54,6 @@
     private final boolean mCollapsePanelAfterManageButton;
 
     private CarNotificationViewAdapter mAdapter;
-    private Context mContext;
     private LinearLayoutManager mLayoutManager;
     private NotificationClickHandlerFactory mClickHandlerFactory;
     private NotificationDataManager mNotificationDataManager;
@@ -65,10 +64,12 @@
     private RecyclerView mListView;
     private Button mManageButton;
     private TextView mEmptyNotificationHeaderText;
+    private Button mClearAllButton;
+    private CarNotificationItemTouchListener mItemTouchListener;
+    private OnScrollListener mScrollListener;
 
     public CarNotificationView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mContext = context;
         mNotificationDataManager = NotificationDataManager.getInstance();
         mCollapsePanelAfterManageButton = context.getResources().getBoolean(
                 R.bool.config_collapseShadePanelAfterManageButtonPress);
@@ -84,23 +85,36 @@
         mListView = findViewById(R.id.notifications);
 
         mListView.setClipChildren(false);
-        mLayoutManager = new LinearLayoutManager(mContext);
+        Context context = getContext();
+        mLayoutManager = new LinearLayoutManager(context);
         mListView.setLayoutManager(mLayoutManager);
         mListView.addItemDecoration(new TopAndBottomOffsetDecoration(
-                mContext.getResources().getDimensionPixelSize(R.dimen.item_spacing)));
+                context.getResources().getDimensionPixelSize(R.dimen.item_spacing)));
         mListView.addItemDecoration(new ItemSpacingDecoration(
-                mContext.getResources().getDimensionPixelSize(R.dimen.item_spacing)));
-        mAdapter = new CarNotificationViewAdapter(mContext, /* isGroupNotificationAdapter= */
+                context.getResources().getDimensionPixelSize(R.dimen.item_spacing)));
+        mAdapter = new CarNotificationViewAdapter(context, /* isGroupNotificationAdapter= */
                 false, this::startClearAllNotifications);
-        mListView.setAdapter(mAdapter);
 
-        mUxrContentLimiter = new UxrContentLimiterImpl(mContext, R.xml.uxr_config);
+        mUxrContentLimiter = new UxrContentLimiterImpl(context, R.xml.uxr_config);
+
+        mEmptyNotificationHeaderText = findViewById(R.id.empty_notification_text);
+        mManageButton = findViewById(R.id.manage_button);
+
+        mClearAllButton = findViewById(R.id.clear_all_button);
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
         mUxrContentLimiter.setAdapter(mAdapter);
         mUxrContentLimiter.start();
+        mListView.setAdapter(mAdapter);
 
-        mListView.addOnItemTouchListener(new CarNotificationItemTouchListener(mContext, mAdapter));
+        mItemTouchListener = new CarNotificationItemTouchListener(getContext(), mAdapter);
+        mListView.addOnItemTouchListener(mItemTouchListener);
 
-        mListView.addOnScrollListener(new OnScrollListener() {
+        mScrollListener = new OnScrollListener() {
             @Override
             public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                 super.onScrollStateChanged(recyclerView, newState);
@@ -109,7 +123,9 @@
                     setVisibleNotificationsAsSeen();
                 }
             }
-        });
+        };
+        mListView.addOnScrollListener(mScrollListener);
+
         mListView.setItemAnimator(new DefaultItemAnimator(){
             @Override
             public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder
@@ -124,13 +140,32 @@
             }
         });
 
-        Button clearAllButton = findViewById(R.id.clear_all_button);
-        mEmptyNotificationHeaderText = findViewById(R.id.empty_notification_text);
-        mManageButton = findViewById(R.id.manage_button);
         mManageButton.setOnClickListener(this::manageButtonOnClickListener);
+        if (mClearAllButton != null) {
+            mClearAllButton.setOnClickListener(v -> startClearAllNotifications());
+        }
+    }
 
-        if (clearAllButton != null) {
-            clearAllButton.setOnClickListener(v -> startClearAllNotifications());
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        // TODO b/301492797 also set the adapter in the UxrContentLimiter to null
+        mUxrContentLimiter.stop();
+
+        mListView.setAdapter(null);
+
+        if (mItemTouchListener != null) {
+            mListView.removeOnItemTouchListener(mItemTouchListener);
+        }
+        if (mScrollListener != null) {
+            mListView.removeOnScrollListener(mScrollListener);
+        }
+        mListView.setItemAnimator(null);
+        mManageButton.setOnClickListener(null);
+
+        if (mClearAllButton != null) {
+            mClearAllButton.setOnClickListener(null);
         }
     }
 
@@ -344,7 +379,7 @@
                 R.integer.clear_all_notifications_animation_delay_interval_ms);
         for (int i = 0; i < dismissibleNotificationViews.size(); i++) {
             View currentView = dismissibleNotificationViews.get(i);
-            ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(mContext,
+            ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(getContext(),
                     R.animator.clear_all_animate_out);
             animator.setTarget(currentView);
 
@@ -436,8 +471,8 @@
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
         intent.addCategory(Intent.CATEGORY_DEFAULT);
-        mContext.startActivityAsUser(intent,
-                UserHandle.of(NotificationUtils.getCurrentUser(mContext)));
+        getContext().startActivityAsUser(intent,
+                UserHandle.of(NotificationUtils.getCurrentUser(getContext())));
 
         if (mClickHandlerFactory != null && mCollapsePanelAfterManageButton) {
             mClickHandlerFactory.collapsePanel();
diff --git a/src/com/android/car/notification/CarNotificationViewAdapter.java b/src/com/android/car/notification/CarNotificationViewAdapter.java
index 01ce566..4885bd7 100644
--- a/src/com/android/car/notification/CarNotificationViewAdapter.java
+++ b/src/com/android/car/notification/CarNotificationViewAdapter.java
@@ -31,6 +31,7 @@
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.car.notification.PreprocessingManager.CallStateListener;
 import com.android.car.notification.template.CarNotificationBaseViewHolder;
 import com.android.car.notification.template.CarNotificationFooterViewHolder;
 import com.android.car.notification.template.CarNotificationHeaderViewHolder;
@@ -68,6 +69,7 @@
     // book keeping expanded notification groups
     private final List<ExpandedNotification> mExpandedNotifications = new ArrayList<>();
     private final CarNotificationItemController mNotificationItemController;
+    private final CallStateListener mCallStateListener = this::onCallStateChanged;
 
     private List<NotificationGroup> mNotifications = new ArrayList<>();
     private Map<String, Integer> mGroupKeyToCountMap = new HashMap<>();
@@ -105,20 +107,20 @@
         if (!mIsGroupNotificationAdapter) {
             mViewPool = new RecyclerView.RecycledViewPool();
         }
-
-        PreprocessingManager.getInstance(context).addCallStateListener(this::onCallStateChanged);
     }
 
     @Override
     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
         super.onAttachedToRecyclerView(recyclerView);
         mLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
+        PreprocessingManager.getInstance(mContext).addCallStateListener(mCallStateListener);
     }
 
     @Override
     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
         super.onDetachedFromRecyclerView(recyclerView);
         mLayoutManager = null;
+        PreprocessingManager.getInstance(mContext).removeCallStateListener(mCallStateListener);
     }
 
     @Override
diff --git a/tests/unit/src/com/android/car/notification/CarNotificationViewTest.java b/tests/unit/src/com/android/car/notification/CarNotificationViewTest.java
index 41c52de..40e3b2a 100644
--- a/tests/unit/src/com/android/car/notification/CarNotificationViewTest.java
+++ b/tests/unit/src/com/android/car/notification/CarNotificationViewTest.java
@@ -103,6 +103,7 @@
 
     @Test
     public void onClickClearAllButton_callsFactoryClearNotificationsWithDismissibleNotifications() {
+        mCarNotificationView.onAttachedToWindow();
         Button clearAllButton = mCarNotificationView.findViewById(R.id.clear_all_button);
         NotificationGroup dismissible = getNotificationGroup(/* isDismissible= */ true);
         NotificationGroup notDismissible = getNotificationGroup(/* isDismissible= */ false);
@@ -120,6 +121,7 @@
 
     @Test
     public void onClickManageButton_actionNotificationSettings() {
+        mCarNotificationView.onAttachedToWindow();
         Button manageButton = mCarNotificationView.findViewById(R.id.manage_button);
 
         manageButton.callOnClick();
@@ -131,6 +133,7 @@
 
     @Test
     public void onClickManageButton_categoryDefault() {
+        mCarNotificationView.onAttachedToWindow();
         Button manageButton = mCarNotificationView.findViewById(R.id.manage_button);
 
         manageButton.callOnClick();
@@ -142,6 +145,7 @@
 
     @Test
     public void onClickManageButton_flagsNewTaskAndMultipleTask() {
+        mCarNotificationView.onAttachedToWindow();
         Button manageButton = mCarNotificationView.findViewById(R.id.manage_button);
 
         manageButton.callOnClick();