Adding AppsStore for handling various app updates
Change-Id: Ia2242ce583576ace0924ef7142793ba37f4adcb9
This commit is contained in:
+1
-42
@@ -31,48 +31,7 @@
|
||||
|
||||
<include layout="@layout/all_apps_fast_scroller" />
|
||||
|
||||
<com.android.launcher3.allapps.FloatingHeaderView
|
||||
android:id="@+id/all_apps_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/all_apps_header_top_padding"
|
||||
android:clipToPadding="false"
|
||||
android:layout_below="@id/search_container_all_apps" >
|
||||
|
||||
<include layout="@layout/predictions_view" android:id="@+id/header_content" />
|
||||
|
||||
<com.android.launcher3.allapps.PersonalWorkSlidingTabStrip
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/all_apps_header_tab_height"
|
||||
android:layout_marginLeft="@dimen/all_apps_tabs_side_padding"
|
||||
android:layout_marginRight="@dimen/all_apps_tabs_side_padding"
|
||||
android:layout_below="@id/header_content"
|
||||
android:orientation="horizontal">
|
||||
<Button
|
||||
android:id="@+id/tab_personal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="@string/all_apps_personal_tab"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/all_apps_tab_text"
|
||||
android:textSize="14sp"/>
|
||||
<Button
|
||||
android:id="@+id/tab_work"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="@string/all_apps_work_tab"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/all_apps_work_tab_text"
|
||||
android:textSize="14sp"/>
|
||||
</com.android.launcher3.allapps.PersonalWorkSlidingTabStrip>
|
||||
</com.android.launcher3.allapps.FloatingHeaderView>
|
||||
<include layout="@layout/all_apps_floating_header" />
|
||||
|
||||
<!-- Note: we are reusing/repurposing a system attribute for search layout, because of a
|
||||
platform bug, which prevents using custom attributes in <include> tag -->
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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.
|
||||
-->
|
||||
<com.android.launcher3.allapps.FloatingHeaderView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/all_apps_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/search_container_all_apps"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="@dimen/all_apps_header_top_padding"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<com.android.launcher3.allapps.PersonalWorkSlidingTabStrip
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/all_apps_header_tab_height"
|
||||
android:layout_marginLeft="@dimen/all_apps_tabs_side_padding"
|
||||
android:layout_marginRight="@dimen/all_apps_tabs_side_padding"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/tab_personal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="@string/all_apps_personal_tab"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/all_apps_tab_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/tab_work"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="@string/all_apps_work_tab"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/all_apps_work_tab_text"
|
||||
android:textSize="14sp" />
|
||||
</com.android.launcher3.allapps.PersonalWorkSlidingTabStrip>
|
||||
</com.android.launcher3.allapps.FloatingHeaderView>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<com.android.launcher3.allapps.PredictionRowView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
@@ -55,13 +55,11 @@ import com.android.launcher3.dragndrop.DragController;
|
||||
import com.android.launcher3.dragndrop.DragOptions;
|
||||
import com.android.launcher3.keyboard.FocusedItemDecorator;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
|
||||
import com.android.launcher3.util.ComponentKey;
|
||||
import com.android.launcher3.util.ComponentKeyMapper;
|
||||
import com.android.launcher3.util.ItemInfoMatcher;
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
import com.android.launcher3.views.BottomUserEducationView;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -76,6 +74,7 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
private final ClickShadowView mTouchFeedbackView;
|
||||
private final ItemInfoMatcher mPersonalMatcher = ItemInfoMatcher.ofUser(Process.myUserHandle());
|
||||
private final ItemInfoMatcher mWorkMatcher = ItemInfoMatcher.not(mPersonalMatcher);
|
||||
private final AllAppsStore mAllAppsStore = new AllAppsStore();
|
||||
|
||||
private SearchUiManager mSearchUiManager;
|
||||
private View mSearchContainer;
|
||||
@@ -92,8 +91,6 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
private boolean mHasPredictions = false;
|
||||
private boolean mSearchModeWhileUsingTabs = false;
|
||||
|
||||
private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
|
||||
|
||||
public AllAppsContainerView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
@@ -132,6 +129,10 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
// TODO: Reimplement once fast scroller is fixed.
|
||||
}
|
||||
|
||||
public AllAppsStore getAppsStore() {
|
||||
return mAllAppsStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
@@ -149,64 +150,29 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
public void setApps(List<AppInfo> apps) {
|
||||
boolean hasWorkProfileApp = hasWorkProfileApp(apps);
|
||||
rebindAdapters(hasWorkProfileApp);
|
||||
mComponentToAppMap.clear();
|
||||
addOrUpdateApps(apps);
|
||||
mAllAppsStore.setApps(apps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or updates existing apps in the list
|
||||
*/
|
||||
public void addOrUpdateApps(List<AppInfo> apps) {
|
||||
for (AppInfo app : apps) {
|
||||
mComponentToAppMap.put(app.toComponentKey(), app);
|
||||
}
|
||||
onAppsUpdated();
|
||||
mSearchUiManager.refreshSearchResult();
|
||||
mHeader.onAppsUpdated();
|
||||
mAllAppsStore.addOrUpdateApps(apps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes some apps from the list.
|
||||
*/
|
||||
public void removeApps(List<AppInfo> apps) {
|
||||
for (AppInfo app : apps) {
|
||||
mComponentToAppMap.remove(app.toComponentKey());
|
||||
}
|
||||
onAppsUpdated();
|
||||
mSearchUiManager.refreshSearchResult();
|
||||
}
|
||||
|
||||
private void onAppsUpdated() {
|
||||
for (int i = 0; i < getNumOfAdapters(); i++) {
|
||||
mAH[i].appsList.onAppsUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
private int getNumOfAdapters() {
|
||||
return mUsingTabs ? mAH.length : 1;
|
||||
mAllAppsStore.removeApps(apps);
|
||||
}
|
||||
|
||||
public void updatePromiseAppProgress(PromiseAppInfo app) {
|
||||
for (int i = 0; i < mAH.length; i++) {
|
||||
updatePromiseAppProgress(app, mAH[i].recyclerView);
|
||||
}
|
||||
if (isHeaderVisible()) {
|
||||
updatePromiseAppProgress(app, mHeader.getPredictionRow());
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePromiseAppProgress(PromiseAppInfo app, ViewGroup parent) {
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
int childCount = parent.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View child = parent.getChildAt(i);
|
||||
if (child instanceof BubbleTextView && child.getTag() == app) {
|
||||
BubbleTextView bubbleTextView = (BubbleTextView) child;
|
||||
bubbleTextView.applyProgressLevel(app.level);
|
||||
mAllAppsStore.updateAllIcons((child) -> {
|
||||
if (child.getTag() == app) {
|
||||
child.applyProgressLevel(app.level);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,34 +324,15 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
}
|
||||
|
||||
public void updateIconBadges(Set<PackageUserKey> updatedBadges) {
|
||||
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
|
||||
for (int j = 0; j < mAH.length; j++) {
|
||||
updateIconBadges(updatedBadges, packageUserKey, mAH[j].recyclerView);
|
||||
}
|
||||
if (mHeader != null) {
|
||||
updateIconBadges(updatedBadges, packageUserKey, mHeader.getPredictionRow());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateIconBadges(Set<PackageUserKey> updatedBadges, PackageUserKey packageUserKey,
|
||||
ViewGroup parent) {
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
final int n = parent.getChildCount();
|
||||
for (int i = 0; i < n; i++) {
|
||||
View child = parent.getChildAt(i);
|
||||
if (child instanceof PredictionRowView) {
|
||||
updateIconBadges(updatedBadges, packageUserKey, (PredictionRowView) child);
|
||||
PackageUserKey tempKey = new PackageUserKey(null, null);
|
||||
mAllAppsStore.updateAllIcons((child) -> {
|
||||
if (child.getTag() instanceof ItemInfo) {
|
||||
ItemInfo info = (ItemInfo) child.getTag();
|
||||
if (tempKey.updateFromItemInfo(info) && updatedBadges.contains(tempKey)) {
|
||||
child.applyBadgeState(info, true /* animate */);
|
||||
}
|
||||
}
|
||||
if (!(child instanceof BubbleTextView) || !(child.getTag() instanceof ItemInfo)) {
|
||||
continue;
|
||||
}
|
||||
ItemInfo info = (ItemInfo) child.getTag();
|
||||
if (packageUserKey.updateFromItemInfo(info) && updatedBadges.contains(packageUserKey)) {
|
||||
((BubbleTextView) child).applyBadgeState(info, true /* animate */);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public SpringAnimationHandler getSpringAnimationHandler() {
|
||||
@@ -403,6 +350,9 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
replaceRVContainer(showTabs);
|
||||
mUsingTabs = showTabs;
|
||||
|
||||
mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.MAIN].recyclerView);
|
||||
mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.WORK].recyclerView);
|
||||
|
||||
if (mUsingTabs) {
|
||||
mAH[AdapterHolder.MAIN].setup(mViewPager.getChildAt(0), mPersonalMatcher);
|
||||
mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkMatcher);
|
||||
@@ -419,6 +369,9 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
}
|
||||
}
|
||||
|
||||
mAllAppsStore.registerIconContainer(mAH[AdapterHolder.MAIN].recyclerView);
|
||||
mAllAppsStore.registerIconContainer(mAH[AdapterHolder.WORK].recyclerView);
|
||||
|
||||
applyTouchDelegate();
|
||||
}
|
||||
|
||||
@@ -492,9 +445,6 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
}
|
||||
|
||||
public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
|
||||
if (isHeaderVisible()) {
|
||||
mHeader.getPredictionRow().setPredictedApps(apps);
|
||||
}
|
||||
mAH[AdapterHolder.MAIN].appsList.setPredictedApps(apps);
|
||||
boolean hasPredictions = !apps.isEmpty();
|
||||
if (mHasPredictions != hasPredictions) {
|
||||
@@ -506,7 +456,7 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
}
|
||||
|
||||
public AppInfo findApp(ComponentKeyMapper<AppInfo> mapper) {
|
||||
return mapper.getItem(mComponentToAppMap);
|
||||
return mAllAppsStore.getApp(mapper);
|
||||
}
|
||||
|
||||
public AlphabeticalAppsList getApps() {
|
||||
@@ -526,9 +476,9 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
return;
|
||||
}
|
||||
mHeader.setVisibility(View.VISIBLE);
|
||||
mHeader.setup(mAH, mComponentToAppMap, mNumPredictedAppsPerRow);
|
||||
mHeader.setup(mAH, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView == null);
|
||||
|
||||
int padding = mHeader.getPredictionRow().getExpectedHeight();
|
||||
int padding = mHeader.getMaxTranslation();
|
||||
if (mHasPredictions && !mUsingTabs) {
|
||||
padding += mHeader.getPaddingTop() + mHeader.getPaddingBottom();
|
||||
}
|
||||
@@ -582,14 +532,6 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
}
|
||||
}
|
||||
|
||||
public List<AppInfo> getPredictedApps() {
|
||||
if (isHeaderVisible()) {
|
||||
return mHeader.getPredictionRow().getPredictedApps();
|
||||
} else {
|
||||
return mAH[AdapterHolder.MAIN].appsList.getPredictedApps();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHeaderVisible() {
|
||||
return mHeader != null && mHeader.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
@@ -604,7 +546,7 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
public static final int MAIN = 0;
|
||||
public static final int WORK = 1;
|
||||
|
||||
final AllAppsGridAdapter adapter;
|
||||
public final AllAppsGridAdapter adapter;
|
||||
final LinearLayoutManager layoutManager;
|
||||
final SpringAnimationHandler animationHandler;
|
||||
final AlphabeticalAppsList appsList;
|
||||
@@ -614,7 +556,7 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
boolean verticalFadingEdge;
|
||||
|
||||
AdapterHolder(boolean isWork) {
|
||||
appsList = new AlphabeticalAppsList(mLauncher, mComponentToAppMap, isWork);
|
||||
appsList = new AlphabeticalAppsList(mLauncher, mAllAppsStore, isWork);
|
||||
adapter = new AllAppsGridAdapter(mLauncher, appsList, mLauncher,
|
||||
AllAppsContainerView.this, true);
|
||||
appsList.setAdapter(adapter);
|
||||
@@ -649,11 +591,6 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
? paddingTopForTabs : padding.top;
|
||||
recyclerView.setPadding(padding.left, paddingTop, padding.right, padding.bottom);
|
||||
}
|
||||
if (isHeaderVisible()) {
|
||||
PredictionRowView prv = mHeader.getPredictionRow();
|
||||
prv.setPadding(padding.left, prv.getPaddingTop() , padding.right,
|
||||
prv.getPaddingBottom());
|
||||
}
|
||||
}
|
||||
|
||||
void applyNumsPerRow() {
|
||||
@@ -663,10 +600,6 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
|
||||
}
|
||||
adapter.setNumAppsPerRow(mNumAppsPerRow);
|
||||
appsList.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
|
||||
if (isHeaderVisible()) {
|
||||
mHeader.getPredictionRow()
|
||||
.setNumAppsPerRow(mNumPredictedAppsPerRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
package com.android.launcher3.allapps;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.android.launcher3.AppInfo;
|
||||
import com.android.launcher3.BubbleTextView;
|
||||
import com.android.launcher3.util.ComponentKey;
|
||||
import com.android.launcher3.util.ComponentKeyMapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A utility class to maintain the collection of all apps.
|
||||
*/
|
||||
public class AllAppsStore {
|
||||
|
||||
private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
|
||||
private final List<OnUpdateListener> mUpdateListeners = new ArrayList<>();
|
||||
private final ArrayList<ViewGroup> mIconContainers = new ArrayList<>();
|
||||
|
||||
public Collection<AppInfo> getApps() {
|
||||
return mComponentToAppMap.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current set of apps.
|
||||
*/
|
||||
public void setApps(List<AppInfo> apps) {
|
||||
mComponentToAppMap.clear();
|
||||
addOrUpdateApps(apps);
|
||||
}
|
||||
|
||||
public AppInfo getApp(ComponentKey key) {
|
||||
return mComponentToAppMap.get(key);
|
||||
}
|
||||
|
||||
public AppInfo getApp(ComponentKeyMapper<AppInfo> mapper) {
|
||||
return mapper.getItem(mComponentToAppMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or updates existing apps in the list
|
||||
*/
|
||||
public void addOrUpdateApps(List<AppInfo> apps) {
|
||||
for (AppInfo app : apps) {
|
||||
mComponentToAppMap.put(app.toComponentKey(), app);
|
||||
}
|
||||
notifyUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes some apps from the list.
|
||||
*/
|
||||
public void removeApps(List<AppInfo> apps) {
|
||||
for (AppInfo app : apps) {
|
||||
mComponentToAppMap.remove(app.toComponentKey());
|
||||
}
|
||||
notifyUpdate();
|
||||
}
|
||||
|
||||
|
||||
private void notifyUpdate() {
|
||||
int count = mUpdateListeners.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
mUpdateListeners.get(i).onAppsUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
public void addUpdateListener(OnUpdateListener listener) {
|
||||
mUpdateListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeUpdateListener(OnUpdateListener listener) {
|
||||
mUpdateListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void registerIconContainer(ViewGroup container) {
|
||||
if (container != null) {
|
||||
mIconContainers.add(container);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterIconContainer(ViewGroup container) {
|
||||
mIconContainers.remove(container);
|
||||
}
|
||||
|
||||
public void updateAllIcons(IconAction action) {
|
||||
for (int i = mIconContainers.size() - 1; i >= 0; i--) {
|
||||
ViewGroup parent = mIconContainers.get(i);
|
||||
int childCount = parent.getChildCount();
|
||||
|
||||
for (int j = 0; j < childCount; j++) {
|
||||
View child = parent.getChildAt(j);
|
||||
if (child instanceof BubbleTextView) {
|
||||
action.apply((BubbleTextView) child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnUpdateListener {
|
||||
void onAppsUpdated();
|
||||
}
|
||||
|
||||
public interface IconAction {
|
||||
void apply(BubbleTextView icon);
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ import java.util.TreeMap;
|
||||
/**
|
||||
* The alphabetically sorted list of applications.
|
||||
*/
|
||||
public class AlphabeticalAppsList {
|
||||
public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener {
|
||||
|
||||
public static final String TAG = "AlphabeticalAppsList";
|
||||
private static final boolean DEBUG = false;
|
||||
@@ -153,7 +153,7 @@ public class AlphabeticalAppsList {
|
||||
|
||||
// The set of apps from the system not including predictions
|
||||
private final List<AppInfo> mApps = new ArrayList<>();
|
||||
private final HashMap<ComponentKey, AppInfo> mComponentToAppMap;
|
||||
private final AllAppsStore mAllAppsStore;
|
||||
|
||||
// The set of filtered apps with the current filter
|
||||
private final List<AppInfo> mFilteredApps = new ArrayList<>();
|
||||
@@ -179,16 +179,13 @@ public class AlphabeticalAppsList {
|
||||
private int mNumAppRowsInAdapter;
|
||||
private ItemInfoMatcher mItemFilter;
|
||||
|
||||
public AlphabeticalAppsList(
|
||||
Context context,
|
||||
HashMap<ComponentKey,
|
||||
AppInfo> componentToAppMap,
|
||||
boolean isWork) {
|
||||
mComponentToAppMap = componentToAppMap;
|
||||
public AlphabeticalAppsList(Context context, AllAppsStore appsStore, boolean isWork) {
|
||||
mAllAppsStore = appsStore;
|
||||
mLauncher = Launcher.getLauncher(context);
|
||||
mIndexer = new AlphabeticIndexCompat(context);
|
||||
mAppNameComparator = new AppInfoComparator(context);
|
||||
mIsWork = isWork;
|
||||
mAllAppsStore.addUpdateListener(this);
|
||||
}
|
||||
|
||||
public void updateItemFilter(ItemInfoMatcher itemFilter) {
|
||||
@@ -283,14 +280,14 @@ public class AlphabeticalAppsList {
|
||||
}
|
||||
|
||||
private List<AppInfo> processPredictedAppComponents(List<ComponentKeyMapper<AppInfo>> components) {
|
||||
if (mComponentToAppMap.isEmpty()) {
|
||||
if (mAllAppsStore.getApps().isEmpty()) {
|
||||
// Apps have not been bound yet.
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<AppInfo> predictedApps = new ArrayList<>();
|
||||
for (ComponentKeyMapper<AppInfo> mapper : components) {
|
||||
AppInfo info = mapper.getItem(mComponentToAppMap);
|
||||
AppInfo info = mAllAppsStore.getApp(mapper);
|
||||
if (info != null) {
|
||||
predictedApps.add(info);
|
||||
} else {
|
||||
@@ -359,11 +356,12 @@ public class AlphabeticalAppsList {
|
||||
/**
|
||||
* Updates internals when the set of apps are updated.
|
||||
*/
|
||||
void onAppsUpdated() {
|
||||
@Override
|
||||
public void onAppsUpdated() {
|
||||
// Sort the list of apps
|
||||
mApps.clear();
|
||||
|
||||
for (AppInfo app : mComponentToAppMap.values()) {
|
||||
for (AppInfo app : mAllAppsStore.getApps()) {
|
||||
if (mItemFilter == null || mItemFilter.matches(app, null) || hasFilter()) {
|
||||
mApps.add(app);
|
||||
}
|
||||
@@ -580,7 +578,7 @@ public class AlphabeticalAppsList {
|
||||
}
|
||||
ArrayList<AppInfo> result = new ArrayList<>();
|
||||
for (ComponentKey key : mSearchResults) {
|
||||
AppInfo match = mComponentToAppMap.get(key);
|
||||
AppInfo match = mAllAppsStore.getApp(key);
|
||||
if (match != null) {
|
||||
result.add(match);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.android.launcher3.allapps;
|
||||
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
@@ -27,18 +26,14 @@ import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import com.android.launcher3.AppInfo;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.util.ComponentKey;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class FloatingHeaderView extends RelativeLayout implements
|
||||
public class FloatingHeaderView extends LinearLayout implements
|
||||
ValueAnimator.AnimatorUpdateListener {
|
||||
|
||||
|
||||
private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0);
|
||||
private final Point mTempOffset = new Point();
|
||||
@@ -63,19 +58,18 @@ public class FloatingHeaderView extends RelativeLayout implements
|
||||
}
|
||||
};
|
||||
|
||||
private PredictionRowView mPredictionRow;
|
||||
private ViewGroup mTabLayout;
|
||||
private AllAppsRecyclerView mMainRV;
|
||||
private AllAppsRecyclerView mWorkRV;
|
||||
private AllAppsRecyclerView mCurrentRV;
|
||||
private ViewGroup mParent;
|
||||
private boolean mTabsHidden;
|
||||
private boolean mHeaderCollapsed;
|
||||
private int mMaxTranslation;
|
||||
private int mSnappedScrolledY;
|
||||
private int mTranslationY;
|
||||
private boolean mForwardToRecyclerView;
|
||||
|
||||
protected int mMaxTranslation;
|
||||
|
||||
public FloatingHeaderView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
@@ -88,17 +82,10 @@ public class FloatingHeaderView extends RelativeLayout implements
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
mTabLayout = findViewById(R.id.tabs);
|
||||
mPredictionRow = findViewById(R.id.header_content);
|
||||
}
|
||||
|
||||
public void setup(AllAppsContainerView.AdapterHolder[] mAH,
|
||||
HashMap<ComponentKey, AppInfo> componentToAppMap, int numPredictedAppsPerRow) {
|
||||
mTabsHidden = mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView == null;
|
||||
mTabLayout.setVisibility(mTabsHidden ? View.GONE : View.VISIBLE);
|
||||
mPredictionRow.setup(mAH[AllAppsContainerView.AdapterHolder.MAIN].adapter,
|
||||
componentToAppMap, numPredictedAppsPerRow);
|
||||
mPredictionRow.setShowDivider(mTabsHidden);
|
||||
mMaxTranslation = mPredictionRow.getExpectedHeight();
|
||||
public void setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden) {
|
||||
mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE);
|
||||
mMainRV = setupRV(mMainRV, mAH[AllAppsContainerView.AdapterHolder.MAIN].recyclerView);
|
||||
mWorkRV = setupRV(mWorkRV, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView);
|
||||
mParent = (ViewGroup) mMainRV.getParent();
|
||||
@@ -117,12 +104,12 @@ public class FloatingHeaderView extends RelativeLayout implements
|
||||
mCurrentRV = active ? mMainRV : mWorkRV;
|
||||
}
|
||||
|
||||
public PredictionRowView getPredictionRow() {
|
||||
return mPredictionRow;
|
||||
public int getMaxTranslation() {
|
||||
return mMaxTranslation;
|
||||
}
|
||||
|
||||
private boolean canSnapAt(int currentScrollY) {
|
||||
return Math.abs(currentScrollY) <= mPredictionRow.getHeight();
|
||||
return Math.abs(currentScrollY) <= mMaxTranslation;
|
||||
}
|
||||
|
||||
private void moved(final int currentScrollY) {
|
||||
@@ -149,16 +136,12 @@ public class FloatingHeaderView extends RelativeLayout implements
|
||||
}
|
||||
}
|
||||
|
||||
private void apply() {
|
||||
protected void applyScroll(int uncappedY, int currentY) { }
|
||||
|
||||
protected void apply() {
|
||||
int uncappedTranslationY = mTranslationY;
|
||||
mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
|
||||
if (mTranslationY != uncappedTranslationY) {
|
||||
// we hide it completely if already capped (for opening search anim)
|
||||
mPredictionRow.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
mPredictionRow.setVisibility(View.VISIBLE);
|
||||
mPredictionRow.setTranslationY(uncappedTranslationY);
|
||||
}
|
||||
applyScroll(uncappedTranslationY, mTranslationY);
|
||||
mTabLayout.setTranslationY(mTranslationY);
|
||||
mClip.top = mMaxTranslation + mTranslationY;
|
||||
// clipping on a draw might cause additional redraw
|
||||
@@ -218,10 +201,6 @@ public class FloatingHeaderView extends RelativeLayout implements
|
||||
p.x = getLeft() - mCurrentRV.getLeft() - mParent.getLeft();
|
||||
p.y = getTop() - mCurrentRV.getTop() - mParent.getTop();
|
||||
}
|
||||
|
||||
public void onAppsUpdated() {
|
||||
mPredictionRow.onAppsUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
* 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.allapps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.android.launcher3.AppInfo;
|
||||
import com.android.launcher3.BubbleTextView;
|
||||
import com.android.launcher3.ItemInfo;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.logging.UserEventDispatcher;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto;
|
||||
import com.android.launcher3.util.ComponentKey;
|
||||
import com.android.launcher3.util.ComponentKeyMapper;
|
||||
import com.android.launcher3.util.Themes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class PredictionRowView extends LinearLayout implements
|
||||
UserEventDispatcher.LogContainerProvider {
|
||||
|
||||
private static final String TAG = "PredictionRowView";
|
||||
|
||||
private HashMap<ComponentKey, AppInfo> mComponentToAppMap;
|
||||
private int mNumPredictedAppsPerRow;
|
||||
// The set of predicted app component names
|
||||
private final List<ComponentKeyMapper<AppInfo>> mPredictedAppComponents = new ArrayList<>();
|
||||
// The set of predicted apps resolved from the component names and the current set of apps
|
||||
private final ArrayList<AppInfo> mPredictedApps = new ArrayList<>();
|
||||
private final Paint mPaint;
|
||||
// This adapter is only used to create an identical item w/ same behavior as in the all apps RV
|
||||
private AllAppsGridAdapter mAdapter;
|
||||
private boolean mShowDivider;
|
||||
|
||||
public PredictionRowView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public PredictionRowView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setOrientation(LinearLayout.HORIZONTAL);
|
||||
setWillNotDraw(false);
|
||||
mPaint = new Paint();
|
||||
mPaint.setColor(Themes.getAttrColor(context, android.R.attr.colorControlHighlight));
|
||||
mPaint.setStrokeWidth(getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
|
||||
}
|
||||
|
||||
public void setup(AllAppsGridAdapter adapter, HashMap<ComponentKey, AppInfo> componentToAppMap,
|
||||
int numPredictedAppsPerRow) {
|
||||
mAdapter = adapter;
|
||||
mComponentToAppMap = componentToAppMap;
|
||||
mNumPredictedAppsPerRow = numPredictedAppsPerRow;
|
||||
setVisibility(mPredictedAppComponents.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getExpectedHeight(),
|
||||
MeasureSpec.EXACTLY));
|
||||
}
|
||||
|
||||
public int getExpectedHeight() {
|
||||
int height = 0;
|
||||
if (!mPredictedAppComponents.isEmpty()) {
|
||||
height += Launcher.getLauncher(getContext())
|
||||
.getDeviceProfile().allAppsCellHeightPx;
|
||||
height += getPaddingTop() + getPaddingBottom();
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setShowDivider(boolean showDivider) {
|
||||
mShowDivider = showDivider;
|
||||
int paddingBottom = showDivider ? getResources()
|
||||
.getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height) : 0;
|
||||
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), paddingBottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of apps per row.
|
||||
*/
|
||||
public void setNumAppsPerRow(int numPredictedAppsPerRow) {
|
||||
if (mNumPredictedAppsPerRow != numPredictedAppsPerRow) {
|
||||
mNumPredictedAppsPerRow = numPredictedAppsPerRow;
|
||||
onPredictionsUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the predicted apps.
|
||||
*/
|
||||
public List<AppInfo> getPredictedApps() {
|
||||
return mPredictedApps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current set of predicted apps.
|
||||
*
|
||||
* This can be called before we get the full set of applications, we should merge the results
|
||||
* only in onPredictionsUpdated() which is idempotent.
|
||||
*
|
||||
* If the number of predicted apps is the same as the previous list of predicted apps,
|
||||
* we can optimize by swapping them in place.
|
||||
*/
|
||||
public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
|
||||
mPredictedAppComponents.clear();
|
||||
mPredictedAppComponents.addAll(apps);
|
||||
mPredictedApps.clear();
|
||||
mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
|
||||
onPredictionsUpdated();
|
||||
}
|
||||
|
||||
private void onPredictionsUpdated() {
|
||||
int childCountBefore = getChildCount();
|
||||
if (getChildCount() != mNumPredictedAppsPerRow) {
|
||||
while (getChildCount() > mNumPredictedAppsPerRow) {
|
||||
removeViewAt(0);
|
||||
}
|
||||
while (getChildCount() < mNumPredictedAppsPerRow) {
|
||||
AllAppsGridAdapter.ViewHolder holder = mAdapter
|
||||
.onCreateViewHolder(this, AllAppsGridAdapter.VIEW_TYPE_ICON);
|
||||
BubbleTextView icon = (BubbleTextView) holder.itemView;
|
||||
LinearLayout.LayoutParams params =
|
||||
new LayoutParams(0, icon.getLayoutParams().height);
|
||||
params.weight = 1;
|
||||
icon.setLayoutParams(params);
|
||||
addView(icon);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
BubbleTextView icon = (BubbleTextView) getChildAt(i);
|
||||
icon.reset();
|
||||
if (mPredictedApps.size() > i) {
|
||||
icon.setVisibility(View.VISIBLE);
|
||||
icon.applyFromApplicationInfo(mPredictedApps.get(i));
|
||||
} else {
|
||||
icon.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
if (getChildCount() > 0 && childCountBefore == 0
|
||||
|| getChildCount() == 0 && childCountBefore > 0) {
|
||||
// setting up header to adjust the height
|
||||
// only necessary if childcount switches from/to 0
|
||||
Launcher.getLauncher(getContext()).getAppsView().setupHeader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the app icons in the row view, while preserving the same set of predictions.
|
||||
*/
|
||||
public void onAppsUpdated() {
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
View child = getChildAt(i);
|
||||
if (!(child instanceof BubbleTextView)) {
|
||||
continue;
|
||||
}
|
||||
if (i >= mPredictedApps.size()) {
|
||||
break;
|
||||
}
|
||||
BubbleTextView icon = (BubbleTextView) getChildAt(i);
|
||||
icon.reset();
|
||||
icon.applyFromApplicationInfo(mPredictedApps.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private List<AppInfo> processPredictedAppComponents(
|
||||
List<ComponentKeyMapper<AppInfo>> components) {
|
||||
if (mComponentToAppMap.isEmpty()) {
|
||||
// Apps have not been bound yet.
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<AppInfo> predictedApps = new ArrayList<>();
|
||||
for (ComponentKeyMapper<AppInfo> mapper : components) {
|
||||
AppInfo info = mapper.getItem(mComponentToAppMap);
|
||||
if (info != null) {
|
||||
predictedApps.add(info);
|
||||
} else {
|
||||
if (FeatureFlags.IS_DOGFOOD_BUILD) {
|
||||
Log.e(TAG, "Predicted app not found: " + mapper);
|
||||
}
|
||||
}
|
||||
// Stop at the number of predicted apps
|
||||
if (predictedApps.size() == mNumPredictedAppsPerRow) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return predictedApps;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (mShowDivider) {
|
||||
int side = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
|
||||
int y = getHeight() - (getPaddingBottom() / 2);
|
||||
int x1 = getPaddingLeft() + side;
|
||||
int x2 = getWidth() - getPaddingRight() - side;
|
||||
canvas.drawLine(x1, y, x2, y, mPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
|
||||
LauncherLogProto.Target targetParent) {
|
||||
for (int i = 0; i < mPredictedApps.size(); i++) {
|
||||
AppInfo appInfo = mPredictedApps.get(i);
|
||||
if (appInfo == info) {
|
||||
targetParent.containerType = LauncherLogProto.ContainerType.PREDICTION;
|
||||
target.predictedRank = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,6 @@ public interface SearchUiManager {
|
||||
*/
|
||||
@NonNull SpringAnimation getSpringForFling();
|
||||
|
||||
/**
|
||||
* Notifies the search manager that the apps-list has changed and the search UI should be
|
||||
* updated accordingly.
|
||||
*/
|
||||
void refreshSearchResult();
|
||||
|
||||
/**
|
||||
* Notifies the search manager to close any active search session.
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.android.launcher3.ExtendedEditText;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.allapps.AllAppsContainerView;
|
||||
import com.android.launcher3.allapps.AllAppsStore;
|
||||
import com.android.launcher3.allapps.AlphabeticalAppsList;
|
||||
import com.android.launcher3.allapps.SearchUiManager;
|
||||
import com.android.launcher3.graphics.TintedDrawableSpan;
|
||||
@@ -48,7 +49,8 @@ import java.util.ArrayList;
|
||||
* Layout to contain the All-apps search UI.
|
||||
*/
|
||||
public class AppsSearchContainerLayout extends FrameLayout
|
||||
implements SearchUiManager, AllAppsSearchBarController.Callbacks {
|
||||
implements SearchUiManager, AllAppsSearchBarController.Callbacks,
|
||||
AllAppsStore.OnUpdateListener {
|
||||
|
||||
private final Launcher mLauncher;
|
||||
private final int mMinHeight;
|
||||
@@ -110,6 +112,18 @@ public class AppsSearchContainerLayout extends FrameLayout
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
mLauncher.getAppsView().getAppsStore().addUpdateListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
mLauncher.getAppsView().getAppsStore().removeUpdateListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
|
||||
@@ -134,7 +148,7 @@ public class AppsSearchContainerLayout extends FrameLayout
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshSearchResult() {
|
||||
public void onAppsUpdated() {
|
||||
mSearchBarController.refreshSearchResult();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ package com.android.launcher3.util;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ComponentKeyMapper<T> {
|
||||
|
||||
Reference in New Issue
Block a user