LineageParts: Fix animation for search results

* Current implementation of the highlight when clicking a search result
  pointing to a LineageParts setting isn't working at all
* Kang the widget from settings and adapt it accordingly

Test:
Before:
Search for any AOSP setting (not title)
Click it -> it highlights (by flashing)
Search for any setting from LineageOS, e.g. "arrow keys"
Click it -> won't highlight

After:
Both results behave the same

Change-Id: Ib7e5edc3c62084a8217aa3270cd71cc57389f8a1
diff --git a/res/color/preference_highlight_color.xml b/res/color/preference_highlight_color.xml
new file mode 100644
index 0000000..f8d54d4
--- /dev/null
+++ b/res/color/preference_highlight_color.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2018 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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:alpha="0.26" android:color="?android:attr/colorAccent" />
+</selector>
\ No newline at end of file
diff --git a/res/values/ids.xml b/res/values/ids.xml
new file mode 100644
index 0000000..0b526c5
--- /dev/null
+++ b/res/values/ids.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The LineageOS 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.
+-->
+
+<resources>
+    <item type="id" name="preference_highlighted" />
+</resources>
diff --git a/src/com/shiftos/shiftparts/SettingsPreferenceFragment.java b/src/com/shiftos/shiftparts/SettingsPreferenceFragment.java
index 68c2c13..5874556 100644
--- a/src/com/shiftos/shiftparts/SettingsPreferenceFragment.java
+++ b/src/com/shiftos/shiftparts/SettingsPreferenceFragment.java
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2020 The LineageOS Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -49,6 +50,7 @@
 import com.shiftos.shiftparts.widget.CustomDialogPreference;
 import com.shiftos.shiftparts.widget.DialogCreatable;
 import com.shiftos.shiftparts.widget.FloatingActionButton;
+import com.shiftos.shiftparts.widget.HighlightablePreferenceGroupAdapter;
 import com.shiftos.shiftparts.widget.LayoutPreference;
 
 import java.util.Arrays;
@@ -263,8 +265,11 @@
     }
 
     public void highlightPreferenceIfNeeded() {
-        if (isAdded() && !mPreferenceHighlighted &&!TextUtils.isEmpty(mPreferenceKey)) {
-            highlightPreference(mPreferenceKey);
+        if (!isAdded()) {
+            return;
+        }
+        if (mAdapter != null) {
+            mAdapter.requestHighlight(getView(), getListView());
         }
     }
 
@@ -362,24 +367,6 @@
         return mEmptyView;
     }
 
-    /**
-     * Return a valid ListView position or -1 if none is found
-     */
-    private int canUseListViewForHighLighting(String key) {
-        if (getListView() == null) {
-            return -1;
-        }
-
-        RecyclerView listView = getListView();
-        RecyclerView.Adapter adapter = listView.getAdapter();
-
-        if (adapter != null && adapter instanceof PreferenceGroupAdapter) {
-            return findListPositionFromKey((PreferenceGroupAdapter) adapter, key);
-        }
-
-        return -1;
-    }
-
     @Override
     public RecyclerView.LayoutManager onCreateLayoutManager() {
         mLayoutManager = new LinearLayoutManager(getContext());
@@ -388,7 +375,12 @@
 
     @Override
     protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
-        mAdapter = new HighlightablePreferenceGroupAdapter(preferenceScreen);
+        final Bundle arguments = getArguments();
+        mAdapter = new HighlightablePreferenceGroupAdapter(preferenceScreen,
+                arguments == null
+                        ? null
+                        : arguments.getString(PartsActivity.EXTRA_FRAGMENT_ARG_KEY),
+                mPreferenceHighlighted);
         return mAdapter;
     }
 
@@ -422,21 +414,6 @@
         return mPreferenceCache.size();
     }
 
-    private void highlightPreference(String key) {
-        final int position = canUseListViewForHighLighting(key);
-        if (position >= 0) {
-            mPreferenceHighlighted = true;
-            mLayoutManager.scrollToPosition(position);
-
-            getView().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    mAdapter.highlight(position);
-                }
-            }, DELAY_HIGHLIGHT_DURATION_MILLIS);
-        }
-    }
-
     private int findListPositionFromKey(PreferenceGroupAdapter adapter, String key) {
         final int count = adapter.getItemCount();
         for (int n = 0; n < count; n++) {
@@ -776,34 +753,4 @@
             return false;
         }
     }
-
-    public static class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
-
-        private int mHighlightPosition = -1;
-
-        public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup) {
-            super(preferenceGroup);
-        }
-
-        public void highlight(int position) {
-            mHighlightPosition = position;
-            notifyDataSetChanged();
-        }
-
-        @Override
-        public void onBindViewHolder(PreferenceViewHolder holder, int position) {
-            super.onBindViewHolder(holder, position);
-            if (position == mHighlightPosition) {
-                View v = holder.itemView;
-                if (v.getBackground() != null) {
-                    final int centerX = v.getWidth() / 2;
-                    final int centerY = v.getHeight() / 2;
-                    v.getBackground().setHotspot(centerX, centerY);
-                }
-                v.setPressed(true);
-                v.setPressed(false);
-                mHighlightPosition = -1;
-            }
-        }
-    }
 }
diff --git a/src/com/shiftos/shiftparts/widget/HighlightablePreferenceGroupAdapter.java b/src/com/shiftos/shiftparts/widget/HighlightablePreferenceGroupAdapter.java
new file mode 100644
index 0000000..1f229f3
--- /dev/null
+++ b/src/com/shiftos/shiftparts/widget/HighlightablePreferenceGroupAdapter.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ * Copyright (C) 2020 The LineageOS 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 com.shiftos.shiftparts.widget;
+
+import static com.shiftos.shiftparts.PartsActivity.EXTRA_FRAGMENT_ARG_KEY;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceGroupAdapter;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.PreferenceViewHolder;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.shiftos.shiftparts.R;
+import com.shiftos.shiftparts.SettingsPreferenceFragment;
+
+public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
+
+    private static final String TAG = "HighlightableAdapter";
+    @VisibleForTesting
+    static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L;
+    private static final long HIGHLIGHT_DURATION = 15000L;
+    private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L;
+    private static final long HIGHLIGHT_FADE_IN_DURATION = 200L;
+
+    @VisibleForTesting
+    final int mHighlightColor;
+    @VisibleForTesting
+    boolean mFadeInAnimated;
+
+    private final int mNormalBackgroundRes;
+    private final String mHighlightKey;
+    private boolean mHighlightRequested;
+    private int mHighlightPosition = RecyclerView.NO_POSITION;
+
+    public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key,
+            boolean highlightRequested) {
+        super(preferenceGroup);
+        mHighlightKey = key;
+        mHighlightRequested = highlightRequested;
+        final Context context = preferenceGroup.getContext();
+        final TypedValue outValue = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
+                outValue, true /* resolveRefs */);
+        mNormalBackgroundRes = outValue.resourceId;
+        mHighlightColor = context.getColor(R.color.preference_highlight_color);
+    }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder, int position) {
+        super.onBindViewHolder(holder, position);
+        updateBackground(holder, position);
+    }
+
+    @VisibleForTesting
+    void updateBackground(PreferenceViewHolder holder, int position) {
+        View v = holder.itemView;
+        if (position == mHighlightPosition) {
+            // This position should be highlighted. If it's highlighted before - skip animation.
+            addHighlightBackground(v, !mFadeInAnimated);
+        } else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
+            // View with highlight is reused for a view that should not have highlight
+            removeHighlightBackground(v, false /* animate */);
+        }
+    }
+
+    public void requestHighlight(View root, RecyclerView recyclerView) {
+        if (mHighlightRequested || recyclerView == null || TextUtils.isEmpty(mHighlightKey)) {
+            return;
+        }
+        root.postDelayed(() -> {
+            final int position = getPreferenceAdapterPosition(mHighlightKey);
+            if (position < 0) {
+                return;
+            }
+            mHighlightRequested = true;
+            recyclerView.smoothScrollToPosition(position);
+            mHighlightPosition = position;
+            notifyItemChanged(position);
+        }, DELAY_HIGHLIGHT_DURATION_MILLIS);
+    }
+
+    public boolean isHighlightRequested() {
+        return mHighlightRequested;
+    }
+
+    @VisibleForTesting
+    void requestRemoveHighlightDelayed(View v) {
+        v.postDelayed(() -> {
+            mHighlightPosition = RecyclerView.NO_POSITION;
+            removeHighlightBackground(v, true /* animate */);
+        }, HIGHLIGHT_DURATION);
+    }
+
+    private void addHighlightBackground(View v, boolean animate) {
+        v.setTag(R.id.preference_highlighted, true);
+        if (!animate) {
+            v.setBackgroundColor(mHighlightColor);
+            Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background");
+            requestRemoveHighlightDelayed(v);
+            return;
+        }
+        mFadeInAnimated = true;
+        final int colorFrom = Color.WHITE;
+        final int colorTo = mHighlightColor;
+        final ValueAnimator fadeInLoop = ValueAnimator.ofObject(
+                new ArgbEvaluator(), colorFrom, colorTo);
+        fadeInLoop.setDuration(HIGHLIGHT_FADE_IN_DURATION);
+        fadeInLoop.addUpdateListener(
+                animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
+        fadeInLoop.setRepeatMode(ValueAnimator.REVERSE);
+        fadeInLoop.setRepeatCount(4);
+        fadeInLoop.start();
+        Log.d(TAG, "AddHighlight: starting fade in animation");
+        requestRemoveHighlightDelayed(v);
+    }
+
+    private void removeHighlightBackground(View v, boolean animate) {
+        if (!animate) {
+            v.setTag(R.id.preference_highlighted, false);
+            v.setBackgroundResource(mNormalBackgroundRes);
+            Log.d(TAG, "RemoveHighlight: No animation requested - setting normal background");
+            return;
+        }
+
+        if (!Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
+            // Not highlighted, no-op
+            Log.d(TAG, "RemoveHighlight: Not highlighted - skipping");
+            return;
+        }
+        int colorFrom = mHighlightColor;
+        int colorTo = Color.WHITE;
+
+        v.setTag(R.id.preference_highlighted, false);
+        final ValueAnimator colorAnimation = ValueAnimator.ofObject(
+                new ArgbEvaluator(), colorFrom, colorTo);
+        colorAnimation.setDuration(HIGHLIGHT_FADE_OUT_DURATION);
+        colorAnimation.addUpdateListener(
+                animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
+        colorAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                // Animation complete - the background is now white. Change to mNormalBackgroundRes
+                // so it is white and has ripple on touch.
+                v.setBackgroundResource(mNormalBackgroundRes);
+            }
+        });
+        colorAnimation.start();
+        Log.d(TAG, "Starting fade out animation");
+    }
+}