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

Commit fe45ead7 authored by Hyunyoung Song's avatar Hyunyoung Song
Browse files

[automerger] Remove flicker when multiple apps are added/removed/updated on...

[automerger] Remove flicker when multiple apps are added/removed/updated on widget tray am: 8e5464b5

Change-Id: I070a617828dedc19cbacefd6dd6c15337c1b85d6
parents c9dadbdd 8e5464b5
Loading
Loading
Loading
Loading
+4 −0
Original line number Original line Diff line number Diff line
@@ -41,4 +41,8 @@ public class WidgetListRowEntry {
        this.widgets = items;
        this.widgets = items;
    }
    }


    @Override
    public String toString() {
        return pkgItem.packageName + ":" + widgets.size();
    }
}
}
+9 −2
Original line number Original line Diff line number Diff line
@@ -17,10 +17,12 @@
package com.android.launcher3.widget;
package com.android.launcher3.widget;


import android.content.Context;
import android.content.Context;
import android.content.pm.LauncherApps;
import android.graphics.Point;
import android.graphics.Point;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.util.AttributeSet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup;
import android.widget.Toast;
import android.widget.Toast;
@@ -31,8 +33,10 @@ import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.Utilities;
import com.android.launcher3.compat.AlphabeticIndexCompat;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.model.PackageItemInfo;
import com.android.launcher3.model.PackageItemInfo;
@@ -74,7 +78,11 @@ public class WidgetsContainerView extends BaseContainerView
    public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
    public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        super(context, attrs, defStyleAttr);
        mLauncher = Launcher.getLauncher(context);
        mLauncher = Launcher.getLauncher(context);
        mAdapter = new WidgetsListAdapter(this, this, context);
        LauncherAppState apps = LauncherAppState.getInstance(context);
        mAdapter = new WidgetsListAdapter(context, LayoutInflater.from(context),
                apps.getWidgetCache(), new AlphabeticIndexCompat(context), this, this,
                new WidgetsDiffReporter(apps.getIconCache()));
        mAdapter.setNotifyListener();
        if (LOGD) {
        if (LOGD) {
            Log.d(TAG, "WidgetsContainerView constructor");
            Log.d(TAG, "WidgetsContainerView constructor");
        }
        }
@@ -232,7 +240,6 @@ public class WidgetsContainerView extends BaseContainerView
     */
     */
    public void setWidgets(MultiHashMap<PackageItemInfo, WidgetItem> model) {
    public void setWidgets(MultiHashMap<PackageItemInfo, WidgetItem> model) {
        mAdapter.setWidgets(model);
        mAdapter.setWidgets(model);
        mAdapter.notifyDataSetChanged();


        View loader = getContentView().findViewById(R.id.loader);
        View loader = getContentView().findViewById(R.id.loader);
        if (loader != null) {
        if (loader != null) {
+140 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2017 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 com.android.launcher3.widget;

import android.util.Log;

import com.android.launcher3.IconCache;
import com.android.launcher3.model.PackageItemInfo;
import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator;

import java.util.ArrayList;
import java.util.Iterator;

/**
 * Do diff on widget's tray list items and call the {@link NotifyListener} methods accordingly.
 */
public class WidgetsDiffReporter {
    private final boolean DEBUG = true;
    private final String TAG = "WidgetsDiffReporter";
    private final IconCache mIconCache;
    private NotifyListener mListener;

    public interface NotifyListener {
        void notifyDataSetChanged();
        void notifyItemChanged(int index);
        void notifyItemInserted(int index);
        void notifyItemRemoved(int index);
    }

    public WidgetsDiffReporter(IconCache iconCache) {
        mIconCache = iconCache;
    }

    public void setListener(NotifyListener listener) {
        mListener = listener;
    }

    public void process(ArrayList<WidgetListRowEntry> currentEntries,
            ArrayList<WidgetListRowEntry> newEntries, WidgetListRowEntryComparator comparator) {
        if (DEBUG) {
            Log.d(TAG, "process oldEntries#=" + currentEntries.size()
                    + " newEntries#=" + newEntries.size());
        }
        if (currentEntries.size() == 0 && newEntries.size() > 0) {
            currentEntries.addAll(newEntries);
            mListener.notifyDataSetChanged();
            return;
        }
        ArrayList<WidgetListRowEntry> orgEntries =
                (ArrayList<WidgetListRowEntry>) currentEntries.clone();
        Iterator<WidgetListRowEntry> orgIter = orgEntries.iterator();
        Iterator<WidgetListRowEntry> newIter = newEntries.iterator();

        WidgetListRowEntry orgRowEntry = orgIter.next();
        WidgetListRowEntry newRowEntry = newIter.next();

        do {
            int diff = comparePackageName(orgRowEntry, newRowEntry, comparator);
            if (DEBUG) {
                Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)",
                        diff, orgRowEntry != null? orgRowEntry.toString() : null,
                        newRowEntry != null? newRowEntry.toString() : null));
            }
            int index = -1;
            if (diff < 0) {
                index = currentEntries.indexOf(orgRowEntry);
                mListener.notifyItemRemoved(index);
                if (DEBUG) {
                    Log.d(TAG, String.format("notifyItemRemoved called (%d)%s", index,
                            orgRowEntry.titleSectionName));
                }
                currentEntries.remove(index);
                orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
            } else if (diff > 0) {
                index = orgRowEntry != null? currentEntries.indexOf(orgRowEntry):
                        currentEntries.size();
                currentEntries.add(index, newRowEntry);
                newRowEntry = newIter.hasNext() ? newIter.next() : null;
                mListener.notifyItemInserted(index);
                if (DEBUG) {
                    Log.d(TAG, String.format("notifyItemInserted called (%d)%s", index,
                            newRowEntry.titleSectionName));
                }
            } else {
                // same package name but,
                // did the icon, title, etc, change?
                // or did the widget size and desc, span, etc change?
                if (!isSamePackageItemInfo(orgRowEntry.pkgItem, newRowEntry.pkgItem) ||
                        !orgRowEntry.widgets.equals(newRowEntry.widgets)) {
                    index = currentEntries.indexOf(orgRowEntry);
                    currentEntries.set(index, newRowEntry);
                    mListener.notifyItemChanged(index);
                    if (DEBUG) {
                        Log.d(TAG, String.format("notifyItemChanged called (%d)%s", index,
                                newRowEntry.titleSectionName));
                    }
                }
                orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
                newRowEntry = newIter.hasNext() ? newIter.next() : null;
            }
        } while(orgRowEntry != null || newRowEntry != null);
    }

    /**
     * Compare package name using the same comparator as in {@link WidgetsListAdapter}.
     * Also handle null row pointers.
     */
    private int comparePackageName(WidgetListRowEntry curRow, WidgetListRowEntry newRow,
            WidgetListRowEntryComparator comparator) {
        if (curRow == null && newRow == null) {
            throw new IllegalStateException("Cannot compare PackageItemInfo if both rows are null.");
        }

        if (curRow == null && newRow != null) {
            return 1; // new row needs to be inserted
        } else if (curRow != null && newRow == null) {
            return -1; // old row needs to be deleted
        }
        return comparator.compare(curRow, newRow);
    }

    private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
        return curInfo.iconBitmap.equals(newInfo.iconBitmap) &&
                !mIconCache.isDefaultIcon(curInfo.iconBitmap, curInfo.user);
    }
}
+48 −20
Original line number Original line Diff line number Diff line
@@ -21,9 +21,10 @@ import android.support.v7.widget.RecyclerView.Adapter;
import android.util.Log;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup;


import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.R;
import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.compat.AlphabeticIndexCompat;
import com.android.launcher3.compat.AlphabeticIndexCompat;
@@ -55,40 +56,67 @@ public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> {


    private final WidgetPreviewLoader mWidgetPreviewLoader;
    private final WidgetPreviewLoader mWidgetPreviewLoader;
    private final LayoutInflater mLayoutInflater;
    private final LayoutInflater mLayoutInflater;

    private final View.OnClickListener mIconClickListener;
    private final View.OnLongClickListener mIconLongClickListener;

    private final ArrayList<WidgetListRowEntry> mEntries = new ArrayList<>();
    private final AlphabeticIndexCompat mIndexer;
    private final AlphabeticIndexCompat mIndexer;


    private final OnClickListener mIconClickListener;
    private final OnLongClickListener mIconLongClickListener;
    private final int mIndent;
    private final int mIndent;

    private ArrayList<WidgetListRowEntry> mEntries = new ArrayList<>();
    public WidgetsListAdapter(View.OnClickListener iconClickListener,
    private final WidgetsDiffReporter mDiffReporter;
            View.OnLongClickListener iconLongClickListener,

            Context context) {
    public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
        mLayoutInflater = LayoutInflater.from(context);
            WidgetPreviewLoader widgetPreviewLoader, AlphabeticIndexCompat indexCompat,
        mWidgetPreviewLoader = LauncherAppState.getInstance(context).getWidgetCache();
            OnClickListener iconClickListener, OnLongClickListener iconLongClickListener,

            WidgetsDiffReporter diffReporter) {
        mIndexer = new AlphabeticIndexCompat(context);
        mLayoutInflater = layoutInflater;

        mWidgetPreviewLoader = widgetPreviewLoader;
        mIndexer = indexCompat;
        mIconClickListener = iconClickListener;
        mIconClickListener = iconClickListener;
        mIconLongClickListener = iconLongClickListener;
        mIconLongClickListener = iconLongClickListener;
        mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent);
        mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent);
        mDiffReporter = diffReporter;
    }

    public void setNotifyListener() {
        mDiffReporter.setListener(new WidgetsDiffReporter.NotifyListener() {
            @Override
            public void notifyDataSetChanged() {
                WidgetsListAdapter.this.notifyDataSetChanged();
            }

            @Override
            public void notifyItemChanged(int index) {
                WidgetsListAdapter.this.notifyItemChanged(index);
            }
            }


            @Override
            public void notifyItemInserted(int index) {
                WidgetsListAdapter.this.notifyItemInserted(index);
            }

            @Override
            public void notifyItemRemoved(int index) {
                WidgetsListAdapter.this.notifyItemRemoved(index);
            }
        });
    }

    /**
     * Update the widget list.
     */
    public void setWidgets(MultiHashMap<PackageItemInfo, WidgetItem> widgets) {
    public void setWidgets(MultiHashMap<PackageItemInfo, WidgetItem> widgets) {
        mEntries.clear();
        ArrayList<WidgetListRowEntry> tempEntries = new ArrayList<>();
        WidgetItemComparator widgetComparator = new WidgetItemComparator();


        WidgetItemComparator widgetComparator = new WidgetItemComparator();
        for (Map.Entry<PackageItemInfo, ArrayList<WidgetItem>> entry : widgets.entrySet()) {
        for (Map.Entry<PackageItemInfo, ArrayList<WidgetItem>> entry : widgets.entrySet()) {
            WidgetListRowEntry row = new WidgetListRowEntry(entry.getKey(), entry.getValue());
            WidgetListRowEntry row = new WidgetListRowEntry(entry.getKey(), entry.getValue());
            row.titleSectionName = mIndexer.computeSectionName(row.pkgItem.title);
            row.titleSectionName = mIndexer.computeSectionName(row.pkgItem.title);
            Collections.sort(row.widgets, widgetComparator);
            Collections.sort(row.widgets, widgetComparator);
            mEntries.add(row);
            tempEntries.add(row);
        }
        }

        WidgetListRowEntryComparator rowComparator = new WidgetListRowEntryComparator();
        Collections.sort(mEntries, new WidgetListRowEntryComparator());
        Collections.sort(tempEntries, rowComparator);
        mDiffReporter.process(mEntries, tempEntries, rowComparator);
    }
    }


    @Override
    @Override
+149 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2017 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 com.android.launcher3.widget;

import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.LayoutInflater;

import com.android.launcher3.IconCache;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.compat.AlphabeticIndexCompat;
import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.model.PackageItemInfo;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.util.MultiHashMap;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class WidgetsListAdapterTest {

    private final String TAG = "WidgetsListAdapterTest";

    @Mock private LayoutInflater mMockLayoutInflater;
    @Mock private WidgetPreviewLoader mMockWidgetCache;
    @Mock private WidgetsDiffReporter.NotifyListener mListener;
    @Mock private IconCache mIconCache;

    private WidgetsListAdapter mAdapter;
    private AlphabeticIndexCompat mIndexCompat;
    private InvariantDeviceProfile mTestProfile;
    private Context mContext;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mContext = InstrumentationRegistry.getTargetContext();
        mTestProfile = new InvariantDeviceProfile();
        mTestProfile.numRows = 5;
        mTestProfile.numColumns = 5;
        mIndexCompat = new AlphabeticIndexCompat(mContext);
        WidgetsDiffReporter reporter = new WidgetsDiffReporter(mIconCache);
        reporter.setListener(mListener);
        mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
                mIndexCompat, null, null, reporter);
    }

    @Test
    public void test_notifyDataSetChanged() throws Exception {
        mAdapter.setWidgets(generateSampleMap(1));
        verify(mListener, times(1)).notifyDataSetChanged();
    }

    @Test
    public void test_notifyItemInserted() throws Exception {
        mAdapter.setWidgets(generateSampleMap(1));
        mAdapter.setWidgets(generateSampleMap(2));
        verify(mListener, times(1)).notifyDataSetChanged();
        verify(mListener, times(1)).notifyItemInserted(1);
    }

    @Test
    public void test_notifyItemRemoved() throws Exception {
        mAdapter.setWidgets(generateSampleMap(2));
        mAdapter.setWidgets(generateSampleMap(1));
        verify(mListener, times(1)).notifyDataSetChanged();
        verify(mListener, times(1)).notifyItemRemoved(1);
    }

    @Test
    public void testNotifyItemChanged_PackageIconDiff() throws Exception {
        mAdapter.setWidgets(generateSampleMap(1));
        mAdapter.setWidgets(generateSampleMap(1));
        verify(mListener, times(1)).notifyDataSetChanged();
        verify(mListener, times(1)).notifyItemChanged(0);
    }

    @Test
    public void testNotifyItemChanged_widgetItemInfoDiff() throws Exception {
        // TODO: same package name but item number changed
    }

    @Test
    public void testNotifyItemInsertedRemoved_hodgepodge() throws Exception {
        // TODO: insert and remove combined.          curMap
        // newMap [A, C, D]                           [A, B, E]
        // B - C < 0, removed B from index 1          [A, E]
        // E - C > 0, C inserted to index 1           [A, C, E]
        // E - D > 0, D inserted to index 2           [A, C, D, E]
        // E - null = -1, E deleted from index 3      [A, C, D]
    }

    /**
     * Helper method to generate the sample widget model map that can be used for the tests
     * @param num the number of WidgetItem the map should contain
     * @return
     */
    private MultiHashMap<PackageItemInfo, WidgetItem> generateSampleMap(int num) {
        MultiHashMap<PackageItemInfo, WidgetItem> newMap = new MultiHashMap();
        if (num <= 0) return newMap;

        PackageManager pm = mContext.getPackageManager();
        AppWidgetManagerCompat widgetManager = AppWidgetManagerCompat.getInstance(mContext);
        for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(null)) {
            WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo
                    .fromProviderInfo(mContext, widgetInfo), pm, mTestProfile);

            PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName());
            pInfo.title = pInfo.packageName;
            pInfo.user = wi.user;
            pInfo.iconBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
            newMap.addToList(pInfo, wi);
            if (newMap.size() == num) {
                break;
            }
        }
        return newMap;
    }
}