blob: 2bb95cb50fb370e7155e45681caa3df322b9f574 [file] [log] [blame]
/*
* Copyright (C) 2015 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.Bitmap;
import android.graphics.Rect;
import android.os.Process;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import com.android.launcher3.AppInfo;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.ClickShadowView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.Insettable;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.PromiseAppInfo;
import com.android.launcher3.R;
import com.android.launcher3.anim.SpringAnimationHandler;
import com.android.launcher3.config.FeatureFlags;
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.util.TransformingTouchDelegate;
import com.android.launcher3.views.SlidingTabStrip;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
/**
* The all apps view container.
*/
public class AllAppsContainerView extends RelativeLayout implements DragSource,
View.OnLongClickListener, Insettable, DeviceProfile.LauncherLayoutChangeListener,
BubbleTextView.BubbleTextShadowHandler {
protected final Rect mBasePadding = new Rect();
private final Launcher mLauncher;
private final AdapterHolder[] mAH;
private final ClickShadowView mTouchFeedbackView;
private final ItemInfoMatcher mPersonalMatcher = ItemInfoMatcher.ofUser(Process.myUserHandle());
private final ItemInfoMatcher mWorkMatcher = ItemInfoMatcher.not(mPersonalMatcher);
private SearchUiManager mSearchUiManager;
private View mSearchContainer;
private InterceptingViewPager mViewPager;
private ViewGroup mHeader;
private FloatingHeaderHandler mFloatingHeaderHandler;
private SpannableStringBuilder mSearchQueryBuilder = null;
private int mNumAppsPerRow;
private int mNumPredictedAppsPerRow;
private TransformingTouchDelegate mTouchDelegate;
private boolean mUsingTabs;
private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
public AllAppsContainerView(Context context) {
this(context, null);
}
public AllAppsContainerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mLauncher = Launcher.getLauncher(context);
mSearchQueryBuilder = new SpannableStringBuilder();
Selection.setSelection(mSearchQueryBuilder, 0);
mTouchFeedbackView = new ClickShadowView(context);
// Make the feedback view large enough to hold the blur bitmap.
int size = mLauncher.getDeviceProfile().allAppsIconSizePx
+ mTouchFeedbackView.getExtraSize();
addView(mTouchFeedbackView, size, size);
mAH = new AdapterHolder[2];
mAH[AdapterHolder.MAIN] = new AdapterHolder();
mAH[AdapterHolder.WORK] = new AdapterHolder();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
grid.addLauncherLayoutChangedListener(this);
applyTouchDelegate();
}
private void applyTouchDelegate() {
RecyclerView rv = getActiveRecyclerView();
mTouchDelegate = new TransformingTouchDelegate(rv);
mTouchDelegate.setBounds(
rv.getLeft() - mBasePadding.left,
rv.getTop() - mBasePadding.top,
rv.getRight() + mBasePadding.right,
rv.getBottom() + mBasePadding.bottom);
setTouchDelegate(mTouchDelegate);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
grid.removeLauncherLayoutChangedListener(this);
}
/**
* Calculate the background padding as it can change due to insets/content padding change.
*/
@Override
public void onLauncherLayoutChanged() {
DeviceProfile grid = mLauncher.getDeviceProfile();
if (!grid.isVerticalBarLayout()) {
return;
}
int[] padding = grid.getContainerPadding();
int paddingLeft = padding[0];
int paddingRight = padding[1];
mBasePadding.set(paddingLeft, 0, paddingRight, 0);
setPadding(paddingLeft, 0, paddingRight, 0);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
applyTouchDelegate();
}
@Override
public void setPressedIcon(BubbleTextView icon, Bitmap background) {
mTouchFeedbackView.setPressedIcon(icon, background);
}
/**
* Sets the current set of apps.
*/
public void setApps(List<AppInfo> apps) {
boolean hasWorkProfileApp = hasWorkProfileApp(apps);
if (mUsingTabs != hasWorkProfileApp) {
rebindAdapters(hasWorkProfileApp);
}
mComponentToAppMap.clear();
addOrUpdateApps(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();
}
/**
* 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;
}
public void updatePromiseAppProgress(PromiseAppInfo app) {
for (int i = 0; i < mAH.length; i++) {
updatePromiseAppProgress(app, mAH[i].recyclerView);
}
if (mFloatingHeaderHandler != null) {
updatePromiseAppProgress(app, mFloatingHeaderHandler.getContentView());
}
}
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);
}
}
}
/**
* Returns whether the view itself will handle the touch event or not.
*/
public boolean shouldContainerScroll(MotionEvent ev) {
// IF the MotionEvent is inside the search box, and the container keeps on receiving
// touch input, container should move down.
if (mLauncher.getDragLayer().isEventOverView(mSearchContainer, ev)) {
return true;
}
if (mUsingTabs && mLauncher.getDragLayer().isEventOverView(mHeader, ev)) {
return true;
}
AllAppsRecyclerView rv = getActiveRecyclerView();
return rv == null || rv.shouldContainerScroll(ev, mLauncher.getDragLayer());
}
public AllAppsRecyclerView getActiveRecyclerView() {
if (!mUsingTabs || mViewPager.getCurrentItem() == 0) {
return mAH[AdapterHolder.MAIN].recyclerView;
} else {
return mAH[AdapterHolder.WORK].recyclerView;
}
}
/**
* Resets the state of AllApps.
*/
public void reset() {
for (int i = 0; i < mAH.length; i++) {
if (mAH[i].recyclerView != null) {
mAH[i].recyclerView.scrollToTop();
}
}
if (mFloatingHeaderHandler != null) {
mFloatingHeaderHandler.reset();
}
// Reset the search bar and base recycler view after transitioning home
mSearchUiManager.reset();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// This is a focus listener that proxies focus from a view into the list view. This is to
// work around the search box from getting first focus and showing the cursor.
setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus && getActiveRecyclerView() != null) {
getActiveRecyclerView().requestFocus();
}
}
});
mHeader = findViewById(R.id.all_apps_header);
mFloatingHeaderHandler = new FloatingHeaderHandler(mHeader);
rebindAdapters(mUsingTabs);
mSearchContainer = findViewById(R.id.search_container_all_apps);
mSearchUiManager = (SearchUiManager) mSearchContainer;
mSearchUiManager.initialize(this);
onLauncherLayoutChanged();
}
public SearchUiManager getSearchUiManager() {
return mSearchUiManager;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
DeviceProfile grid = mLauncher.getDeviceProfile();
// Update the number of items in the grid before we measure the view
grid.updateAppsViewNumCols();
if (mNumAppsPerRow != grid.inv.numColumns ||
mNumPredictedAppsPerRow != grid.inv.numColumns) {
mNumAppsPerRow = grid.inv.numColumns;
mNumPredictedAppsPerRow = grid.inv.numColumns;
for (int i = 0; i < mAH.length; i++) {
mAH[i].applyNumsPerRow();
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
mSearchUiManager.preDispatchKeyEvent(event);
return super.dispatchKeyEvent(event);
}
@Override
public boolean onLongClick(final View v) {
// When we have exited all apps or are in transition, disregard long clicks
if (!mLauncher.isInState(LauncherState.ALL_APPS) ||
mLauncher.getWorkspace().isSwitchingState()) return false;
// Return if global dragging is not enabled or we are already dragging
if (!mLauncher.isDraggingEnabled()) return false;
if (mLauncher.getDragController().isDragging()) return false;
// Start the drag
final DragController dragController = mLauncher.getDragController();
dragController.addDragListener(new DragController.DragListener() {
@Override
public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
v.setVisibility(INVISIBLE);
}
@Override
public void onDragEnd() {
v.setVisibility(VISIBLE);
dragController.removeDragListener(this);
}
});
DeviceProfile grid = mLauncher.getDeviceProfile();
DragOptions options = new DragOptions();
options.intrinsicIconScaleFactor = (float) grid.allAppsIconSizePx / grid.iconSizePx;
mLauncher.getWorkspace().beginDragShared(v, this, options);
return false;
}
@Override
public void onDropCompleted(View target, DragObject d, boolean success) { }
@Override
public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
// This is filled in {@link AllAppsRecyclerView}
}
@Override
public void setInsets(Rect insets) {
DeviceProfile grid = mLauncher.getDeviceProfile();
for (int i = 0; i < mAH.length; i++) {
mAH[i].padding.bottom = insets.bottom;
mAH[i].applyPadding();
}
if (grid.isVerticalBarLayout()) {
ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.leftMargin = insets.left;
mlp.topMargin = insets.top;
mlp.rightMargin = insets.right;
setLayoutParams(mlp);
} else {
View navBarBg = findViewById(R.id.nav_bar_bg);
ViewGroup.LayoutParams navBarBgLp = navBarBg.getLayoutParams();
navBarBgLp.height = insets.bottom;
navBarBg.setLayoutParams(navBarBgLp);
}
}
public void updateIconBadges(Set<PackageUserKey> updatedBadges) {
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
for (int j = 0; j < mAH.length; j++) {
if (mAH[j].recyclerView != null) {
final int n = mAH[j].recyclerView.getChildCount();
for (int i = 0; i < n; i++) {
View child = mAH[j].recyclerView.getChildAt(i);
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() {
return mUsingTabs ? null : mAH[AdapterHolder.MAIN].animationHandler;
}
private void rebindAdapters(boolean showTabs) {
if (showTabs != mUsingTabs) {
replaceRVContainer(showTabs);
}
mUsingTabs = showTabs;
if (mUsingTabs) {
mAH[AdapterHolder.MAIN].setup(mViewPager.getChildAt(0), mPersonalMatcher);
mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkMatcher);
setupWorkProfileTabs();
setupHeader();
} else {
mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null);
if (FeatureFlags.ALL_APPS_PREDICTION_ROW_VIEW) {
setupHeader();
} else {
mFloatingHeaderHandler = null;
mHeader.setVisibility(View.GONE);
}
}
applyTouchDelegate();
}
private boolean hasWorkProfileApp(List<AppInfo> apps) {
if (FeatureFlags.ALL_APPS_TABS_ENABLED) {
for (AppInfo app : apps) {
if (mWorkMatcher.matches(app, null)) {
return true;
}
}
}
return false;
}
private void replaceRVContainer(boolean showTabs) {
for (int i = 0; i < mAH.length; i++) {
if (mAH[i].recyclerView != null) {
mAH[i].recyclerView.setLayoutManager(null);
}
}
View oldView = getRecyclerViewContainer();
int index = indexOfChild(oldView);
removeView(oldView);
int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout;
View newView = LayoutInflater.from(getContext()).inflate(layout, this, false);
addView(newView, index);
mViewPager = showTabs ? (InterceptingViewPager) newView : null;
}
public View getRecyclerViewContainer() {
return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view);
}
private void setupWorkProfileTabs() {
final SlidingTabStrip tabs = findViewById(R.id.tabs);
mViewPager.setAdapter(new TabsPagerAdapter());
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
boolean mVisible = true;
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
tabs.updateIndicatorPosition(position, positionOffset);
if (positionOffset == 0 && !mVisible || positionOffset > 0 && mVisible) {
mVisible = positionOffset == 0;
for (int i = 0; i < mAH.length; i++) {
if (mAH[i].recyclerView != null) {
mAH[i].recyclerView.getScrollbar().setAlpha(mVisible ? 1 : 0);
}
}
}
}
@Override
public void onPageSelected(int pos) {
tabs.updateTabTextColor(pos);
mFloatingHeaderHandler.setMainActive(pos == 0);
applyTouchDelegate();
if (mAH[pos].recyclerView != null) {
mAH[pos].recyclerView.bindFastScrollbar();
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
findViewById(R.id.tab_personal)
.setOnClickListener((View view) -> mViewPager.setCurrentItem(0));
findViewById(R.id.tab_work)
.setOnClickListener((View view) -> mViewPager.setCurrentItem(1));
}
public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
if (mFloatingHeaderHandler != null) {
mFloatingHeaderHandler.getContentView().setPredictedApps(apps);
}
mAH[AdapterHolder.MAIN].appsList.setPredictedApps(apps);
}
public AppInfo findApp(ComponentKeyMapper<AppInfo> mapper) {
return mapper.getItem(mComponentToAppMap);
}
public AlphabeticalAppsList getApps() {
return mAH[AdapterHolder.MAIN].appsList;
}
public boolean isUsingTabs() {
return mUsingTabs;
}
public FloatingHeaderHandler getFloatingHeaderHandler() {
return mFloatingHeaderHandler;
}
private void setupHeader() {
mHeader.setVisibility(View.VISIBLE);
int contentHeight = mLauncher.getDeviceProfile().allAppsCellHeightPx;
if (!mUsingTabs) {
contentHeight += getResources()
.getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height);
}
RecyclerView mainRV = mAH[AdapterHolder.MAIN].recyclerView;
RecyclerView workRV = mAH[AdapterHolder.WORK].recyclerView;
mFloatingHeaderHandler.setup(mainRV, workRV, contentHeight);
mFloatingHeaderHandler.getContentView().setup(mAH[AdapterHolder.MAIN].adapter,
mComponentToAppMap, mNumPredictedAppsPerRow);
int padding = contentHeight;
if (!mUsingTabs) {
padding += mHeader.getPaddingTop() + mHeader.getPaddingBottom();
}
for (int i = 0; i < mAH.length; i++) {
mAH[i].paddingTopForTabs = padding;
mAH[i].applyPadding();
}
}
public void setLastSearchQuery(String query) {
for (int i = 0; i < mAH.length; i++) {
mAH[i].adapter.setLastSearchQuery(query);
}
}
public void onSearchResultsChanged() {
for (int i = 0; i < mAH.length; i++) {
if (mAH[i].recyclerView != null) {
mAH[i].recyclerView.onSearchResultsChanged();
}
}
}
public void setRecyclerViewPaddingTop(int top) {
for (int i = 0; i < mAH.length; i++) {
mAH[i].padding.top = top;
mAH[i].applyPadding();
}
}
public void setRecyclerViewSidePadding(int left, int right) {
for (int i = 0; i < mAH.length; i++) {
mAH[i].padding.left = left;
mAH[i].padding.right = right;
mAH[i].applyPadding();
}
}
public void setRecyclerViewVerticalFadingEdgeEnabled(boolean enabled) {
for (int i = 0; i < mAH.length; i++) {
mAH[i].applyVerticalFadingEdgeEnabled(enabled);
}
}
public void addElevationController(RecyclerView.OnScrollListener scrollListener) {
if (!mUsingTabs) {
mAH[AdapterHolder.MAIN].recyclerView.addOnScrollListener(scrollListener);
}
}
public List<AppInfo> getPredictedApps() {
if (mUsingTabs) {
return mFloatingHeaderHandler.getContentView().getPredictedApps();
} else {
return mAH[AdapterHolder.MAIN].appsList.getPredictedApps();
}
}
public class AdapterHolder {
public static final int MAIN = 0;
public static final int WORK = 1;
final AllAppsGridAdapter adapter;
final LinearLayoutManager layoutManager;
final SpringAnimationHandler animationHandler;
final AlphabeticalAppsList appsList;
final Rect padding = new Rect();
int paddingTopForTabs;
AllAppsRecyclerView recyclerView;
boolean verticalFadingEdge;
AdapterHolder() {
appsList = new AlphabeticalAppsList(mLauncher, mComponentToAppMap);
adapter = new AllAppsGridAdapter(mLauncher, appsList, mLauncher,
AllAppsContainerView.this, true);
appsList.setAdapter(adapter);
animationHandler = adapter.getSpringAnimationHandler();
layoutManager = adapter.getLayoutManager();
}
void setup(@NonNull View rv, @Nullable ItemInfoMatcher matcher) {
appsList.updateItemFilter(matcher);
recyclerView = (AllAppsRecyclerView) rv;
recyclerView.setApps(appsList, mUsingTabs);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(true);
// No animations will occur when changes occur to the items in this RecyclerView.
recyclerView.setItemAnimator(null);
if (FeatureFlags.LAUNCHER3_PHYSICS && animationHandler != null) {
recyclerView.setSpringAnimationHandler(animationHandler);
}
FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(recyclerView);
recyclerView.addItemDecoration(focusedItemDecorator);
recyclerView.preMeasureViews(adapter);
adapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
applyVerticalFadingEdgeEnabled(verticalFadingEdge);
applyPadding();
applyNumsPerRow();
}
void applyPadding() {
if (recyclerView != null) {
int paddingTop = mUsingTabs || FeatureFlags.ALL_APPS_PREDICTION_ROW_VIEW
? paddingTopForTabs : padding.top;
recyclerView.setPadding(padding.left, paddingTop, padding.right, padding.bottom);
}
if (mFloatingHeaderHandler != null) {
mFloatingHeaderHandler.getContentView()
.setPadding(padding.left, 0 , padding.right, 0);
}
}
void applyNumsPerRow() {
if (mNumAppsPerRow > 0) {
if (recyclerView != null) {
recyclerView.setNumAppsPerRow(mLauncher.getDeviceProfile(), mNumAppsPerRow);
}
adapter.setNumAppsPerRow(mNumAppsPerRow);
appsList.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
if (mFloatingHeaderHandler != null) {
mFloatingHeaderHandler.getContentView()
.setNumAppsPerRow(mNumPredictedAppsPerRow);
}
}
}
public void applyVerticalFadingEdgeEnabled(boolean enabled) {
verticalFadingEdge = enabled;
mAH[AdapterHolder.MAIN].recyclerView.setVerticalFadingEdgeEnabled(!mUsingTabs
&& verticalFadingEdge);
}
}
private class TabsPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return 2;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (position == 0) {
return mAH[AdapterHolder.MAIN].recyclerView;
} else {
return mAH[AdapterHolder.WORK].recyclerView;
}
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
if (position == 0) {
return getResources().getString(R.string.all_apps_personal_tab);
} else {
return getResources().getString(R.string.all_apps_work_tab);
}
}
}
}