Group weather widgets under the widget category

Test: manual (see video in bug) and added PackageUserKeyTest
      Also tested pending conversation widget to ensure the right icon
      is shown.
Fix: 201062480
Change-Id: If23c28bd93c54fb1747648309ab3c238a1810902
This commit is contained in:
Steven Ng 2021-09-24 12:39:55 +01:00
parent 9c871ae7b6
commit 43859f10bc
25 changed files with 507 additions and 142 deletions

View File

@ -79,12 +79,13 @@ public class WidgetsModel {
}
public WidgetItem getWidgetProviderInfoByProviderName(
ComponentName providerName) {
ComponentName providerName, UserHandle user) {
return null;
}
/** Returns {@link PackageItemInfo} of a pending widget. */
public static PackageItemInfo newPendingItemInfo(ComponentName provider) {
return new PackageItemInfo(provider.getPackageName());
public static PackageItemInfo newPendingItemInfo(
Context context, ComponentName provider, UserHandle userHandle) {
return new PackageItemInfo(provider.getPackageName(), userHandle);
}
}

View File

@ -258,4 +258,18 @@
<declare-styleable name="WidgetsListRowHeader">
<attr name="appIconSize" format="dimension" />
</declare-styleable>
<declare-styleable name="WidgetSections">
<!-- Component name of an app widget provider. -->
<attr name="provider" format="string" />
<!-- If true, keep the app widget under its app listing in addition to the widget category
in the widget picker. Defaults to false if not specified. -->
<attr name="alsoKeepInApp" format="boolean" />
<!-- The category of an app widget provider. Defaults to -1 if not specified. -->
<attr name="category" format="integer" />
<!-- The title name of a widget category. -->
<attr name="sectionTitle" format="reference" />
<!-- The icon drawable of a widget category. -->
<attr name="sectionDrawable" format="reference" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2021 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.
-->
<widget-sections xmlns:launcher="http://schemas.android.com/apk/res-auto">
<section
launcher:category="0"
launcher:sectionDrawable="@drawable/ic_conversations_widget_category"
launcher:sectionTitle="@string/widget_category_conversations">
<widget launcher:provider="com.android.systemui/.people.widget.PeopleSpaceWidgetProvider" />
</section>
</widget-sections>

View File

@ -349,7 +349,7 @@ public class LauncherPreviewRenderer extends ContextWrapper
private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
info.providerName);
info.providerName, info.user);
if (widgetItem == null) {
return;
}

View File

@ -18,6 +18,7 @@ package com.android.launcher3.icons;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
import static java.util.stream.Collectors.groupingBy;
@ -46,7 +47,6 @@ import androidx.annotation.NonNull;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherFiles;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
@ -63,6 +63,8 @@ import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.util.InstantAppResolver;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.widget.WidgetSections;
import com.android.launcher3.widget.WidgetSections.WidgetSection;
import java.util.Collections;
import java.util.List;
@ -275,7 +277,8 @@ public class IconCache extends BaseIconCache {
getTitleAndIcon(appInfo, false);
return appInfo.bitmap;
} else {
PackageItemInfo pkgInfo = new PackageItemInfo(shortcutInfo.getPackage());
PackageItemInfo pkgInfo = new PackageItemInfo(shortcutInfo.getPackage(),
shortcutInfo.getUserHandle());
getTitleAndIconForApp(pkgInfo, false);
return pkgInfo.bitmap;
}
@ -409,8 +412,10 @@ public class IconCache extends BaseIconCache {
CacheEntry entry = getEntryForPackageLocked(
infoInOut.packageName, infoInOut.user, useLowResIcon);
applyCacheEntry(entry, infoInOut);
if (infoInOut.category == PackageItemInfo.CONVERSATIONS) {
infoInOut.title = mContext.getString(R.string.widget_category_conversations);
if (infoInOut.widgetCategory != NO_CATEGORY) {
WidgetSection widgetSection = WidgetSections.getWidgetSections(mContext)
.get(infoInOut.widgetCategory);
infoInOut.title = mContext.getString(widgetSection.mSectionTitle);
infoInOut.contentDescription = mPackageManager.getUserBadgedLabel(
infoInOut.title, infoInOut.user);
}

View File

@ -804,8 +804,9 @@ public class LoaderTask implements Runnable {
if (appWidgetInfo.restoreStatus !=
LauncherAppWidgetInfo.RESTORE_COMPLETED) {
appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo(
appWidgetInfo.providerName);
appWidgetInfo.pendingItemInfo.user = appWidgetInfo.user;
mApp.getContext(),
appWidgetInfo.providerName,
appWidgetInfo.user);
mIconCache.getTitleAndIconForApp(
appWidgetInfo.pendingItemInfo, false);
}

View File

@ -16,47 +16,41 @@
package com.android.launcher3.model.data;
import androidx.annotation.IntDef;
import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
import android.os.UserHandle;
import com.android.launcher3.LauncherSettings;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/**
* Represents a {@link Package} in the widget tray section.
*/
public class PackageItemInfo extends ItemInfoWithIcon {
@Retention(RetentionPolicy.SOURCE)
@IntDef({NO_CATEGORY, CONVERSATIONS})
public @interface Category{}
/** The package is not categorized in the widget tray. */
public static final int NO_CATEGORY = 0;
/** The package is categorized to conversations widget in the widget tray. */
public static final int CONVERSATIONS = 1;
/**
* Package name of the {@link PackageItemInfo}.
*/
public final String packageName;
/** Represents a widget category shown in the widget tray section. */
@Category public final int category;
public final int widgetCategory;
public PackageItemInfo(String packageName) {
this(packageName, NO_CATEGORY);
public PackageItemInfo(String packageName, UserHandle user) {
this(packageName, NO_CATEGORY, user);
}
public PackageItemInfo(String packageName, @Category int category) {
public PackageItemInfo(String packageName, int widgetCategory, UserHandle user) {
this.packageName = packageName;
this.category = category;
this.widgetCategory = widgetCategory;
this.user = user;
this.itemType = LauncherSettings.Favorites.ITEM_TYPE_NON_ACTIONABLE;
}
public PackageItemInfo(PackageItemInfo copy) {
this.packageName = copy.packageName;
this.category = copy.category;
this.widgetCategory = copy.widgetCategory;
this.user = copy.user;
this.itemType = LauncherSettings.Favorites.ITEM_TYPE_NON_ACTIONABLE;
}
@ -75,11 +69,13 @@ public class PackageItemInfo extends ItemInfoWithIcon {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PackageItemInfo that = (PackageItemInfo) o;
return Objects.equals(packageName, that.packageName);
return Objects.equals(packageName, that.packageName)
&& Objects.equals(user, that.user)
&& widgetCategory == that.widgetCategory;
}
@Override
public int hashCode() {
return Objects.hash(packageName, user);
return Objects.hash(packageName, user, widgetCategory);
}
}

View File

@ -1,19 +1,24 @@
package com.android.launcher3.util;
import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.PackageItemInfo;
import java.util.Arrays;
import java.util.Objects;
/** Creates a hash key based on package name and user. */
/** Creates a hash key based on package name, widget category, and user. */
public class PackageUserKey {
public String mPackageName;
public int mWidgetCategory;
public UserHandle mUser;
private int mHashCode;
@ -27,14 +32,31 @@ public class PackageUserKey {
return new PackageUserKey(notification.getPackageName(), notification.getUser());
}
/** Creates a {@link PackageUserKey} from {@link PackageItemInfo}. */
public static PackageUserKey fromPackageItemInfo(PackageItemInfo info) {
if (TextUtils.isEmpty(info.packageName) && info.widgetCategory != NO_CATEGORY) {
return new PackageUserKey(info.widgetCategory, info.user);
}
return new PackageUserKey(info.packageName, info.user);
}
public PackageUserKey(String packageName, UserHandle user) {
update(packageName, user);
}
public PackageUserKey(int widgetCategory, UserHandle user) {
update(/* packageName= */ "", widgetCategory, user);
}
public void update(String packageName, UserHandle user) {
update(packageName, NO_CATEGORY, user);
}
private void update(String packageName, int widgetCategory, UserHandle user) {
mPackageName = packageName;
mWidgetCategory = widgetCategory;
mUser = user;
mHashCode = Arrays.hashCode(new Object[] {packageName, user});
mHashCode = Objects.hash(packageName, widgetCategory, user);
}
/**
@ -59,12 +81,14 @@ public class PackageUserKey {
public boolean equals(Object obj) {
if (!(obj instanceof PackageUserKey)) return false;
PackageUserKey otherKey = (PackageUserKey) obj;
return mPackageName.equals(otherKey.mPackageName) && mUser.equals(otherKey.mUser);
return Objects.equals(mPackageName, otherKey.mPackageName)
&& mWidgetCategory == otherKey.mWidgetCategory
&& Objects.equals(mUser, otherKey.mUser);
}
@NonNull
@Override
public String toString() {
return mPackageName + "#" + mUser;
return mPackageName + "#" + mUser + ",category=" + mWidgetCategory;
}
}

View File

@ -17,7 +17,7 @@
package com.android.launcher3.widget;
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
import static com.android.launcher3.model.data.PackageItemInfo.CONVERSATIONS;
import static com.android.launcher3.widget.WidgetSections.getWidgetSections;
import android.content.Context;
import android.graphics.Canvas;
@ -89,8 +89,8 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView
setOnClickListener(ItemClickHandler.INSTANCE);
if (info.pendingItemInfo == null) {
info.pendingItemInfo = new PackageItemInfo(info.providerName.getPackageName());
info.pendingItemInfo.user = info.user;
info.pendingItemInfo = new PackageItemInfo(info.providerName.getPackageName(),
info.user);
cache.updateIconInBackground(this, info.pendingItemInfo);
} else {
reapplyItemInfo(info.pendingItemInfo);
@ -338,10 +338,11 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView
*/
@Nullable
private Drawable getWidgetCategoryIcon() {
switch (mInfo.pendingItemInfo.category) {
case CONVERSATIONS:
return getContext().getDrawable(R.drawable.ic_conversations_widget_category);
if (mInfo.pendingItemInfo.widgetCategory == WidgetSections.NO_CATEGORY) {
return null;
}
return null;
Context context = getContext();
return context.getDrawable(getWidgetSections(context).get(
mInfo.pendingItemInfo.widgetCategory).mSectionDrawable);
}
}

View File

@ -0,0 +1,134 @@
/*
* Copyright (C) 2021 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 static android.content.res.Resources.ID_NULL;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.Xml;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import com.android.launcher3.R;
import com.android.launcher3.util.IntSet;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.Map;
/** A helper class to parse widget sections (categories) resource overlay. */
public final class WidgetSections {
/** The package is not categorized in the widget tray. */
public static final int NO_CATEGORY = -1;
private static final String TAG_SECTION_NAME = "section";
private static final String TAG_WIDGET_NAME = "widget";
private static SparseArray<WidgetSection> sWidgetSections;
private static Map<ComponentName, IntSet> sWidgetsToCategories;
/** Returns a list of widget sections that are shown in the widget picker. */
public static synchronized SparseArray<WidgetSection> getWidgetSections(Context context) {
if (sWidgetSections != null) {
return sWidgetSections;
}
parseWidgetSectionsXml(context);
return sWidgetSections;
}
/** Returns a map which maps app widget providers to app widget categories. */
public static synchronized Map<ComponentName, IntSet> getWidgetsToCategory(
Context context) {
if (sWidgetsToCategories != null) {
return sWidgetsToCategories;
}
parseWidgetSectionsXml(context);
return sWidgetsToCategories;
}
private static synchronized void parseWidgetSectionsXml(Context context) {
SparseArray<WidgetSection> widgetSections = new SparseArray();
Map<ComponentName, IntSet> widgetsToCategories = new ArrayMap<>();
try (XmlResourceParser parser = context.getResources().getXml(R.xml.widget_sections)) {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG
|| parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if ((type == XmlPullParser.START_TAG)
&& TAG_SECTION_NAME.equals(parser.getName())) {
AttributeSet sectionAttributes = Xml.asAttributeSet(parser);
WidgetSection section = new WidgetSection(context, sectionAttributes);
final int sectionDepth = parser.getDepth();
while (((type = parser.next()) != XmlPullParser.END_TAG
|| parser.getDepth() > sectionDepth)
&& type != XmlPullParser.END_DOCUMENT) {
if ((type == XmlPullParser.START_TAG)
&& TAG_WIDGET_NAME.equals(parser.getName())) {
TypedArray a = context.obtainStyledAttributes(
Xml.asAttributeSet(parser), R.styleable.WidgetSections);
ComponentName provider = ComponentName.unflattenFromString(
a.getString(R.styleable.WidgetSections_provider));
boolean alsoKeepInApp = a.getBoolean(
R.styleable.WidgetSections_alsoKeepInApp,
/* defValue= */ false);
final IntSet categories;
if (widgetsToCategories.containsKey(provider)) {
categories = widgetsToCategories.get(provider);
} else {
categories = new IntSet();
widgetsToCategories.put(provider, categories);
}
if (alsoKeepInApp) {
categories.add(NO_CATEGORY);
}
categories.add(section.mCategory);
}
}
widgetSections.put(section.mCategory, section);
}
}
sWidgetSections = widgetSections;
sWidgetsToCategories = widgetsToCategories;
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException(e);
}
}
/** A data class which contains a widget section's information. */
public static final class WidgetSection {
public final int mCategory;
@StringRes
public final int mSectionTitle;
@DrawableRes
public final int mSectionDrawable;
public WidgetSection(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WidgetSections);
mCategory = a.getInt(R.styleable.WidgetSections_category, NO_CATEGORY);
mSectionTitle = a.getResourceId(R.styleable.WidgetSections_sectionTitle, ID_NULL);
mSectionDrawable = a.getResourceId(R.styleable.WidgetSections_sectionDrawable, ID_NULL);
}
}
}

View File

@ -16,6 +16,8 @@
package com.android.launcher3.widget.model;
import android.os.Process;
import com.android.launcher3.model.data.PackageItemInfo;
import java.util.Collections;
@ -26,7 +28,9 @@ import java.util.Collections;
public class WidgetListSpaceEntry extends WidgetsListBaseEntry {
public WidgetListSpaceEntry() {
super(new PackageItemInfo(""), "", Collections.EMPTY_LIST);
super(new PackageItemInfo(/* packageName= */ "", Process.myUserHandle()),
/* titleSectionName= */ "",
Collections.EMPTY_LIST);
mPkgItem.title = "";
}

View File

@ -24,5 +24,5 @@ public interface OnHeaderClickListener {
/**
* Calls when a header is clicked to show / hide widgets for a package.
*/
void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey);
void onHeaderClicked(boolean showWidgets, PackageUserKey key);
}

View File

@ -98,7 +98,7 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
private Predicate<WidgetsListBaseEntry> mHeaderAndSelectedContentFilter = entry ->
entry instanceof WidgetsListHeaderEntry
|| entry instanceof WidgetsListSearchHeaderEntry
|| new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)
|| PackageUserKey.fromPackageItemInfo(entry.mPkgItem)
.equals(mWidgetsContentVisiblePackageUserKey);
@Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
@Nullable private RecyclerView mRecyclerView;
@ -252,10 +252,11 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
return entry instanceof WidgetsListBaseEntry.Header && matchesKey(entry, key);
}
private static boolean matchesKey(
@NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
private static boolean matchesKey(@NonNull WidgetsListBaseEntry entry,
@Nullable PackageUserKey key) {
if (key == null) return false;
return entry.mPkgItem.packageName.equals(key.mPackageName)
&& entry.mPkgItem.widgetCategory == key.mWidgetCategory
&& entry.mPkgItem.user.equals(key.mUser);
}
@ -434,11 +435,10 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
.filter(entry -> entry instanceof WidgetsListHeaderEntry)
.map(entry -> entry.mPkgItem)
.collect(Collectors.toMap(
entry -> new PackageUserKey(entry.packageName, entry.user),
entry -> PackageUserKey.fromPackageItemInfo(entry),
entry -> entry));
for (WidgetsListBaseEntry visibleEntry: mVisibleEntries) {
PackageUserKey key = new PackageUserKey(visibleEntry.mPkgItem.packageName,
visibleEntry.mPkgItem.user);
PackageUserKey key = PackageUserKey.fromPackageItemInfo(visibleEntry.mPkgItem);
PackageItemInfo packageItemInfo = packagesInfo.get(key);
if (packageItemInfo != null
&& !visibleEntry.mPkgItem.title.equals(packageItemInfo.title)) {

View File

@ -15,6 +15,8 @@
*/
package com.android.launcher3.widget.picker;
import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
@ -41,6 +43,8 @@ import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.util.PluralMessageFormat;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.WidgetSections;
import com.android.launcher3.widget.WidgetSections.WidgetSection;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
@ -173,12 +177,12 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
private void setIcon(PackageItemInfo info) {
Drawable icon;
switch (info.category) {
case PackageItemInfo.CONVERSATIONS:
icon = getContext().getDrawable(R.drawable.ic_conversations_widget_category);
break;
default:
icon = info.newIcon(getContext());
if (info.widgetCategory == NO_CATEGORY) {
icon = info.newIcon(getContext());
} else {
WidgetSection widgetSection = WidgetSections.getWidgetSections(getContext())
.get(info.widgetCategory);
icon = getContext().getDrawable(widgetSection.mSectionDrawable);
}
applyDrawables(icon);
mIconDrawable = icon;

View File

@ -62,9 +62,7 @@ public final class WidgetsListHeaderViewHolderBinder implements
(position & POSITION_LAST) != 0,
/* isExpanded= */ data.isWidgetListShown()));
widgetsListHeader.setOnExpandChangeListener(isExpanded ->
mOnHeaderClickListener.onHeaderClicked(
isExpanded,
new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user)
));
mOnHeaderClickListener.onHeaderClicked(isExpanded,
PackageUserKey.fromPackageItemInfo(data.mPkgItem)));
}
}

View File

@ -64,6 +64,6 @@ public final class WidgetsListSearchHeaderViewHolderBinder implements
/* isExpanded= */ data.isWidgetListShown()));
widgetsListHeader.setOnExpandChangeListener(isExpanded ->
mOnHeaderClickListener.onHeaderClicked(isExpanded,
new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user)));
PackageUserKey.fromPackageItemInfo(data.mPkgItem)));
}
}

View File

@ -4,7 +4,10 @@ package com.android.launcher3.model;
import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_HIDE_FROM_PICKER;
import static com.android.launcher3.pm.ShortcutConfigActivityInfo.queryList;
import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import android.appwidget.AppWidgetProviderInfo;
@ -13,6 +16,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
@ -27,10 +31,12 @@ import com.android.launcher3.icons.ComponentWithLabelAndIcon;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.pm.ShortcutConfigActivityInfo;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
import com.android.launcher3.widget.WidgetSections;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
@ -40,12 +46,12 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Widgets data model that is used by the adapters of the widget views and controllers.
@ -61,9 +67,6 @@ public class WidgetsModel {
private static final String TAG = "WidgetsModel";
private static final boolean DEBUG = false;
private static final ComponentName CONVERSATION_WIDGET = ComponentName.createRelative(
"com.android.systemui", ".people.widget.PeopleSpaceWidgetProvider");
/* Map of widgets and shortcuts that are tracked per package. */
private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsList = new HashMap<>();
@ -168,16 +171,15 @@ public class WidgetsModel {
mWidgetsList.clear();
} else {
// Otherwise, only clear the widgets and shortcuts for the changed package.
mWidgetsList.remove(
packageItemInfoCache.getOrCreate(new WidgetPackageOrCategoryKey(packageUser)));
mWidgetsList.remove(packageItemInfoCache.getOrCreate(packageUser));
}
// add and update.
mWidgetsList.putAll(rawWidgetsShortcuts.stream()
.filter(new WidgetValidityCheck(app))
.collect(Collectors.groupingBy(item ->
packageItemInfoCache.getOrCreate(getWidgetPackageOrCategoryKey(item))
)));
.flatMap(widgetItem -> getPackageUserKeys(app.getContext(), widgetItem).stream()
.map(key -> new Pair<>(packageItemInfoCache.getOrCreate(key), widgetItem)))
.collect(groupingBy(pair -> pair.first, mapping(pair -> pair.second, toList()))));
// Update each package entry
IconCache iconCache = app.getIconCache();
@ -209,9 +211,9 @@ public class WidgetsModel {
}
public WidgetItem getWidgetProviderInfoByProviderName(
ComponentName providerName) {
ComponentName providerName, UserHandle user) {
List<WidgetItem> widgetsList = mWidgetsList.get(
new PackageItemInfo(providerName.getPackageName()));
new PackageItemInfo(providerName.getPackageName(), user));
if (widgetsList == null) {
return null;
}
@ -225,18 +227,40 @@ public class WidgetsModel {
}
/** Returns {@link PackageItemInfo} of a pending widget. */
public static PackageItemInfo newPendingItemInfo(ComponentName provider) {
if (CONVERSATION_WIDGET.equals(provider)) {
return new PackageItemInfo(provider.getPackageName(), PackageItemInfo.CONVERSATIONS);
public static PackageItemInfo newPendingItemInfo(Context context, ComponentName provider,
UserHandle user) {
Map<ComponentName, IntSet> widgetsToCategories =
WidgetSections.getWidgetsToCategory(context);
if (widgetsToCategories.containsKey(provider)) {
Iterator<Integer> categoriesIterator = widgetsToCategories.get(provider).iterator();
int firstCategory = NO_CATEGORY;
while (categoriesIterator.hasNext() && firstCategory == NO_CATEGORY) {
firstCategory = categoriesIterator.next();
}
return new PackageItemInfo(provider.getPackageName(), firstCategory, user);
}
return new PackageItemInfo(provider.getPackageName());
return new PackageItemInfo(provider.getPackageName(), user);
}
private WidgetPackageOrCategoryKey getWidgetPackageOrCategoryKey(WidgetItem item) {
if (CONVERSATION_WIDGET.equals(item.componentName)) {
return new WidgetPackageOrCategoryKey(PackageItemInfo.CONVERSATIONS, item.user);
private List<PackageUserKey> getPackageUserKeys(Context context, WidgetItem item) {
Map<ComponentName, IntSet> widgetsToCategories =
WidgetSections.getWidgetsToCategory(context);
IntSet categories = widgetsToCategories.get(item.componentName);
if (categories == null || categories.isEmpty()) {
return Arrays.asList(
new PackageUserKey(item.componentName.getPackageName(), item.user));
}
return new WidgetPackageOrCategoryKey(item.componentName.getPackageName(), item.user);
List<PackageUserKey> packageUserKeys = new ArrayList<>();
categories.forEach(category -> {
if (category == NO_CATEGORY) {
packageUserKeys.add(
new PackageUserKey(item.componentName.getPackageName(),
item.user));
} else {
packageUserKeys.add(new PackageUserKey(category, item.user));
}
});
return packageUserKeys;
}
private static class WidgetValidityCheck implements Predicate<WidgetItem> {
@ -279,53 +303,13 @@ public class WidgetsModel {
}
}
/** A hash key for grouping widgets by package name or category. */
private static class WidgetPackageOrCategoryKey {
/**
* The package name of the widget provider.
*
* <p>This shouldn't be empty if {@link #mCategory} has a value,
* {@link PackageItemInfo#NO_CATEGORY}.
*/
public final String mPackage;
/** A widget category. */
@PackageItemInfo.Category public final int mCategory;
public final UserHandle mUser;
private final int mHashCode;
WidgetPackageOrCategoryKey(PackageUserKey key) {
this(key.mPackageName, key.mUser);
}
WidgetPackageOrCategoryKey(String packageName, UserHandle user) {
this(packageName, PackageItemInfo.NO_CATEGORY, user);
}
WidgetPackageOrCategoryKey(@PackageItemInfo.Category int category, UserHandle user) {
this("", category, user);
}
private WidgetPackageOrCategoryKey(String packageName,
@PackageItemInfo.Category int category, UserHandle user) {
mPackage = packageName;
mCategory = category;
mUser = user;
mHashCode = Arrays.hashCode(new Object[]{mPackage, mCategory, mUser});
}
@Override
public int hashCode() {
return mHashCode;
}
}
private static final class PackageItemInfoCache {
private final Map<WidgetPackageOrCategoryKey, PackageItemInfo> mMap = new ArrayMap<>();
private final Map<PackageUserKey, PackageItemInfo> mMap = new ArrayMap<>();
PackageItemInfo getOrCreate(WidgetPackageOrCategoryKey key) {
PackageItemInfo getOrCreate(PackageUserKey key) {
PackageItemInfo pInfo = mMap.get(key);
if (pInfo == null) {
pInfo = new PackageItemInfo(key.mPackage, key.mCategory);
pInfo = new PackageItemInfo(key.mPackageName, key.mWidgetCategory, key.mUser);
pInfo.user = key.mUser;
mMap.put(key, pInfo);
}

View File

@ -0,0 +1,162 @@
/*
* Copyright (C) 2021 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.util;
import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
import static com.google.common.truth.Truth.assertThat;
import android.os.UserHandle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.launcher3.model.data.PackageItemInfo;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class PackageUserKeyTest {
@Rule
public ExpectedException exception = ExpectedException.none();
private static final String TEST_PACKAGE = "com.android.test.package";
private static final int CONVERSATIONS = 0;
private static final int WEATHER = 1;
@Test
public void fromPackageItemInfo_shouldCreateExpectedObject() {
PackageUserKey packageUserKey = PackageUserKey.fromPackageItemInfo(
new PackageItemInfo(TEST_PACKAGE, UserHandle.CURRENT));
assertThat(packageUserKey.mPackageName).isEqualTo(TEST_PACKAGE);
assertThat(packageUserKey.mWidgetCategory).isEqualTo(NO_CATEGORY);
assertThat(packageUserKey.mUser).isEqualTo(UserHandle.CURRENT);
}
@Test
public void constructor_packageNameAndUserHandle_shouldCreateExpectedObject() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
assertThat(packageUserKey.mPackageName).isEqualTo(TEST_PACKAGE);
assertThat(packageUserKey.mWidgetCategory).isEqualTo(NO_CATEGORY);
assertThat(packageUserKey.mUser).isEqualTo(UserHandle.CURRENT);
}
@Test
public void constructor_widgetCategoryAndUserHandle_shouldCreateExpectedObject() {
PackageUserKey packageUserKey = new PackageUserKey(CONVERSATIONS, UserHandle.CURRENT);
assertThat(packageUserKey.mPackageName).isEqualTo("");
assertThat(packageUserKey.mWidgetCategory).isEqualTo(CONVERSATIONS);
assertThat(packageUserKey.mUser).isEqualTo(UserHandle.CURRENT);
}
@Test
public void equals_sameObject_shouldReturnTrue() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = packageUserKey;
assertThat(packageUserKey).isEqualTo(otherPackageUserKey);
}
@Test
public void equals_differentObjectSameContent_shouldReturnTrue() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
assertThat(packageUserKey).isEqualTo(otherPackageUserKey);
}
@Test
public void equals_compareAgainstNull_shouldReturnFalse() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
assertThat(packageUserKey).isNotEqualTo(null);
}
@Test
public void equals_differentPackage_shouldReturnFalse() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE + "1",
UserHandle.CURRENT);
assertThat(packageUserKey).isNotEqualTo(otherPackageUserKey);
}
@Test
public void equals_differentCategory_shouldReturnFalse() {
PackageUserKey packageUserKey = new PackageUserKey(WEATHER, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(CONVERSATIONS, UserHandle.CURRENT);
assertThat(packageUserKey).isNotEqualTo(otherPackageUserKey);
}
@Test
public void equals_differentUser_shouldReturnFalse() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.of(1));
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.of(2));
assertThat(packageUserKey).isNotEqualTo(otherPackageUserKey);
}
@Test
public void hashCode_sameObject_shouldBeTheSame() {
PackageUserKey packageUserKey = new PackageUserKey(WEATHER, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = packageUserKey;
assertThat(packageUserKey.hashCode()).isEqualTo(otherPackageUserKey.hashCode());
}
@Test
public void hashCode_differentObjectSameContent_shouldBeTheSame() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
assertThat(packageUserKey.hashCode()).isEqualTo(otherPackageUserKey.hashCode());
}
@Test
public void hashCode_differentPackage_shouldBeDifferent() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE + "1",
UserHandle.CURRENT);
assertThat(packageUserKey.hashCode()).isNotEqualTo(otherPackageUserKey.hashCode());
}
@Test
public void hashCode_differentCategory_shouldBeDifferent() {
PackageUserKey packageUserKey = new PackageUserKey(WEATHER, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(CONVERSATIONS, UserHandle.CURRENT);
assertThat(packageUserKey.hashCode()).isNotEqualTo(otherPackageUserKey.hashCode());
}
@Test
public void hashCode_differentUser_shouldBeDifferent() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.of(1));
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.of(2));
assertThat(packageUserKey.hashCode()).isNotEqualTo(otherPackageUserKey.hashCode());
}
}

View File

@ -288,9 +288,8 @@ public final class WidgetsDiffReporterTest {
private PackageItemInfo createPackageItemInfo(String packageName, String appName,
UserHandle userHandle) {
PackageItemInfo pInfo = new PackageItemInfo(packageName);
PackageItemInfo pInfo = new PackageItemInfo(packageName, userHandle);
pInfo.title = appName;
pInfo.user = userHandle;
pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
return pInfo;
}

View File

@ -215,14 +215,23 @@ public final class WidgetsListAdapterTest {
@Test
public void setWidgetsOnSearch_expandedApp_shouldResetExpandedApp() {
// GIVEN a list of widgets entries:
// [com.google.test0, com.google.test0 content,
// com.google.test1, com.google.test1 content,
// com.google.test2, com.google.test2 content]
// The visible widgets entries: [com.google.test0, com.google.test1, com.google.test2].
ArrayList<WidgetsListBaseEntry> allEntries = generateSampleMap(2);
// [Empty item
// com.google.test0,
// com.google.test0 content,
// com.google.test1,
// com.google.test1 content,
// com.google.test2,
// com.google.test2 content]
// The visible widgets entries:
// [Empty item,
// com.google.test0,
// com.google.test1,
// com.google.test2].
ArrayList<WidgetsListBaseEntry> allEntries = generateSampleMap(3);
mAdapter.setWidgetsOnSearch(allEntries);
// GIVEN com.google.test.1 header is expanded. The visible entries list becomes:
// [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2]
// [Empty item, com.google.test0, com.google.test1, com.google.test1 content,
// com.google.test2]
mAdapter.onHeaderClicked(/* showWidgets= */ true,
new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
Mockito.reset(mListener);
@ -231,7 +240,7 @@ public final class WidgetsListAdapterTest {
mAdapter.setWidgetsOnSearch(allEntries);
// THEN expanded app is reset and the visible entries list becomes:
// [com.google.test0, com.google.test1, com.google.test2]
// [Empty item, com.google.test0, com.google.test1, com.google.test2]
verify(mListener).onItemRangeChanged(eq(2), eq(1), isNull());
verify(mListener).onItemRangeRemoved(/* positionStart= */ 3, /* itemCount= */ 1);
}
@ -257,9 +266,8 @@ public final class WidgetsListAdapterTest {
List<WidgetItem> widgetItems = generateWidgetItems(packageName, /* numOfWidgets= */ 1);
PackageItemInfo pInfo = new PackageItemInfo(packageName);
PackageItemInfo pInfo = new PackageItemInfo(packageName, widgetItems.get(0).user);
pInfo.title = pInfo.packageName;
pInfo.user = widgetItems.get(0).user;
pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
result.add(new WidgetsListHeaderEntry(pInfo, /* titleSectionName= */ "", widgetItems));

View File

@ -30,6 +30,7 @@ import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.UserHandle;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.TextView;
@ -124,12 +125,12 @@ public final class WidgetsListHeaderViewHolderBinderTest {
widgetsListHeader.callOnClick();
verify(mOnHeaderClickListener).onHeaderClicked(eq(true),
eq(new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)));
eq(PackageUserKey.fromPackageItemInfo(entry.mPkgItem)));
}
private WidgetsListHeaderEntry generateSampleAppHeader(String appName, String packageName,
int numOfWidgets) {
PackageItemInfo appInfo = new PackageItemInfo(packageName);
PackageItemInfo appInfo = new PackageItemInfo(packageName, UserHandle.CURRENT);
appInfo.title = appName;
appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);

View File

@ -30,6 +30,7 @@ import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.UserHandle;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.TextView;
@ -124,12 +125,12 @@ public final class WidgetsListSearchHeaderViewHolderBinderTest {
widgetsListHeader.callOnClick();
verify(mOnHeaderClickListener).onHeaderClicked(eq(true),
eq(new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)));
eq(PackageUserKey.fromPackageItemInfo(entry.mPkgItem)));
}
private WidgetsListSearchHeaderEntry generateSampleSearchHeader(String appName,
String packageName, int numOfWidgets) {
PackageItemInfo appInfo = new PackageItemInfo(packageName);
PackageItemInfo appInfo = new PackageItemInfo(packageName, UserHandle.CURRENT);
appInfo.title = appName;
appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);

View File

@ -28,6 +28,7 @@ import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.UserHandle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@ -125,7 +126,7 @@ public final class WidgetsListTableViewHolderBinderTest {
private WidgetsListContentEntry generateSampleAppWithWidgets(String appName, String packageName,
int numOfWidgets) {
PackageItemInfo appInfo = new PackageItemInfo(packageName);
PackageItemInfo appInfo = new PackageItemInfo(packageName, UserHandle.CURRENT);
appInfo.title = appName;
appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);

View File

@ -26,6 +26,7 @@ import static org.mockito.Mockito.doAnswer;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.os.UserHandle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@ -53,8 +54,10 @@ import java.util.Map;
public final class WidgetsListContentEntryTest {
private static final String PACKAGE_NAME = "com.android.test";
private static final String PACKAGE_NAME_2 = "com.android.test2";
private final PackageItemInfo mPackageItemInfo1 = new PackageItemInfo(PACKAGE_NAME);
private final PackageItemInfo mPackageItemInfo2 = new PackageItemInfo(PACKAGE_NAME_2);
private final PackageItemInfo mPackageItemInfo1 = new PackageItemInfo(PACKAGE_NAME,
UserHandle.CURRENT);
private final PackageItemInfo mPackageItemInfo2 = new PackageItemInfo(PACKAGE_NAME_2,
UserHandle.CURRENT);
private final ComponentName mWidget1 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget1");
private final ComponentName mWidget2 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget2");
private final ComponentName mWidget3 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget3");

View File

@ -189,9 +189,8 @@ public class SimpleWidgetsSearchAlgorithmTest {
private PackageItemInfo createPackageItemInfo(String packageName, String appName,
UserHandle userHandle) {
PackageItemInfo pInfo = new PackageItemInfo(packageName);
PackageItemInfo pInfo = new PackageItemInfo(packageName, userHandle);
pInfo.title = appName;
pInfo.user = userHandle;
pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
return pInfo;
}