diff options
Diffstat (limited to 'src')
71 files changed, 9977 insertions, 758 deletions
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index c95d5585a..ca8ae255c 100644 --- a/src/com/android/launcher3/AppInfo.java +++ b/src/com/android/launcher3/AppInfo.java @@ -58,10 +58,11 @@ public class AppInfo extends ItemInfo { public ComponentName componentName; - static final int DOWNLOADED_FLAG = 1; + public static final int DOWNLOADED_FLAG = 1; static final int UPDATED_SYSTEM_APP_FLAG = 2; + static final int REMOTE_APP_FLAG = 4; - int flags = 0; + public int flags = 0; AppInfo() { itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; @@ -85,11 +86,21 @@ public class AppInfo extends ItemInfo { flags = initFlags(info); firstInstallTime = info.getFirstInstallTime(); - iconCache.getTitleAndIcon(this, info, true /* useLowResIcon */); + // Using the full res icon on init might need to be made configurable for low spec devices. + iconCache.getTitleAndIcon(this, info, false /* useLowResIcon */); intent = makeLaunchIntent(context, info, user); this.user = user; } + public AppInfo(Intent intent, String title, UserHandleCompat user) { + this.componentName = intent.getComponent(); + this.container = ItemInfo.NO_ID; + + this.intent = intent; + this.title = title; + this.user = user; + } + public static int initFlags(LauncherActivityInfoCompat info) { int appFlags = info.getApplicationInfo().flags; int flags = 0; @@ -113,6 +124,23 @@ public class AppInfo extends ItemInfo { iconBitmap = info.iconBitmap; } + /** + * Check if this app has a specific flag. + * @param flag flag to check. + * @return true if the flag is present, false otherwise. + */ + public boolean hasFlag(int flag) { + return (flags & flag) != 0; + } + + /** + * Set a flag for this app + * @param flag flag to apply. + */ + public void setFlag(int flag) { + flags |= flag; + } + @Override public String toString() { return "ApplicationInfo(title=" + title + " id=" + this.id @@ -151,4 +179,18 @@ public class AppInfo extends ItemInfo { .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) .putExtra(EXTRA_PROFILE, serialNumber); } + + @Override + public boolean equals(Object o) { + if (o != null && o instanceof AppInfo) { + return componentName.equals(((AppInfo) o).componentName); + } else { + return false; + } + } + + @Override + public int hashCode() { + return componentName.hashCode(); + } } diff --git a/src/com/android/launcher3/AutoExpandTextView.java b/src/com/android/launcher3/AutoExpandTextView.java new file mode 100644 index 000000000..ea7ac896e --- /dev/null +++ b/src/com/android/launcher3/AutoExpandTextView.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2014 Grantland Chew + * Copyright (C) 2015 The CyanogenMod 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; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.method.TransformationMethod; +import android.text.style.ForegroundColorSpan; +import android.util.AttributeSet; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * A single-line TextView that resizes it's letter spacing to fit the width of the view + * + * @author Grantland Chew <grantlandchew@gmail.com> + * @author Linus Lee <llee@cyngn.com> + */ +public class AutoExpandTextView extends TextView { + // How precise we want to be when reaching the target textWidth size + private static final float PRECISION = 0.01f; + + // Attributes + private float mPrecision; + private TextPaint mPaint; + private float[] mPositions; + + public static class HighlightedText { + public String mText; + public boolean mHighlight; + + public HighlightedText(String text, boolean highlight) { + mText = text; + mHighlight = highlight; + } + } + + public AutoExpandTextView(Context context) { + super(context); + init(context, null, 0); + } + + public AutoExpandTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public AutoExpandTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + float precision = PRECISION; + + if (attrs != null) { + TypedArray ta = context.obtainStyledAttributes( + attrs, + R.styleable.AutofitTextView, + defStyle, + 0); + precision = ta.getFloat(R.styleable.AutofitTextView_precision, precision); + } + + mPaint = new TextPaint(); + setPrecision(precision); + } + + /** + * @return the amount of precision used to calculate the correct text size to fit within it's + * bounds. + */ + public float getPrecision() { + return mPrecision; + } + + /** + * Set the amount of precision used to calculate the correct text size to fit within it's + * bounds. Lower precision is more precise and takes more time. + * + * @param precision The amount of precision. + */ + public void setPrecision(float precision) { + if (precision != mPrecision) { + mPrecision = precision; + refitText(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void setLines(int lines) { + super.setLines(1); + refitText(); + } + + /** + * Only allow max lines of 1 + */ + @Override + public void setMaxLines(int maxLines) { + super.setMaxLines(1); + refitText(); + } + + /** + * Re size the font so the specified text fits in the text box assuming the text box is the + * specified width. + */ + private void refitText() { + CharSequence text = getText(); + + if (TextUtils.isEmpty(text)) { + return; + } + + TransformationMethod method = getTransformationMethod(); + if (method != null) { + text = method.getTransformation(text, this); + } + int targetWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + if (targetWidth > 0) { + float high = 100; + float low = 0; + + mPaint.set(getPaint()); + mPaint.setTextSize(getTextSize()); + float letterSpacing = getLetterSpacing(text, mPaint, targetWidth, low, high, + mPrecision); + mPaint.setLetterSpacing(letterSpacing); + calculateSections(text); + + super.setLetterSpacing(letterSpacing); + } + } + + public float getPositionOfSection(int position) { + if (mPositions == null || position >= mPositions.length) { + return 0; + } + return mPositions[position]; + } + + /** + * This calculates the different horizontal positions of each character + */ + private void calculateSections(CharSequence text) { + mPositions = new float[text.length()]; + for (int i = 0; i < text.length(); i++) { + if (i == 0) { + mPositions[0] = mPaint.measureText(text, 0, 1) / 2; + } else { + // try to be lazy and just add the width of the newly added char + mPositions[i] = mPaint.measureText(text, i, i + 1) + mPositions[i - 1]; + } + } + } + + /** + * Sets the list of sections in the text view. This will take the first character of each + * and space it out in the text view using letter spacing + */ + public void setSections(ArrayList<HighlightedText> sections) { + mPositions = null; + if (sections == null || sections.size() == 0) { + setText(""); + return; + } + + Resources r = getContext().getResources(); + int highlightColor = r.getColor(R.color.app_scrubber_highlight_color); + int grayColor = r.getColor(R.color.app_scrubber_gray_color); + + SpannableStringBuilder builder = new SpannableStringBuilder(); + for (HighlightedText highlightText : sections) { + SpannableString spannable = new SpannableString(highlightText.mText.substring(0, 1)); + spannable.setSpan( + new ForegroundColorSpan(highlightText.mHighlight ? highlightColor : grayColor), + 0, spannable.length(), 0); + builder.append(spannable); + } + + setText(builder); + } + + private static float getLetterSpacing(CharSequence text, TextPaint paint, float targetWidth, + float low, float high, float precision) { + float mid = (low + high) / 2.0f; + paint.setLetterSpacing(mid); + + float measuredWidth = paint.measureText(text, 0, text.length()); + + if (high - low < precision) { + if (measuredWidth < targetWidth) { + return mid; + } else { + return low; + } + } else if (measuredWidth > targetWidth) { + return getLetterSpacing(text, paint, targetWidth, low, mid, precision); + } else if (measuredWidth < targetWidth) { + return getLetterSpacing(text, paint, targetWidth, mid, high, precision); + } else { + return mid; + } + } + + @Override + protected void onTextChanged(final CharSequence text, final int start, + final int lengthBefore, final int lengthAfter) { + super.onTextChanged(text, start, lengthBefore, lengthAfter); + refitText(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (w != oldw) { + refitText(); + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/BaseContainerView.java b/src/com/android/launcher3/BaseContainerView.java index c11824054..dfc2d7b9c 100644 --- a/src/com/android/launcher3/BaseContainerView.java +++ b/src/com/android/launcher3/BaseContainerView.java @@ -17,10 +17,16 @@ package com.android.launcher3; import android.content.Context; +import android.content.res.Resources; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; +import android.view.View; +import android.view.ViewStub; import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.settings.SettingsProvider; /** * A base container view, which supports resizing. @@ -43,6 +49,12 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab // The inset to apply to the edges and between the search bar and the container private int mContainerBoundsInset; private boolean mHasSearchBar; + private boolean mUseScroller; + private boolean mUseScrubber; + + protected View mScrubberContainerView; + protected BaseRecyclerViewScrubber mScrubber; + protected final int mScrubberHeight; public BaseContainerView(Context context) { this(context, null); @@ -54,7 +66,9 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab public BaseContainerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mContainerBoundsInset = getResources().getDimensionPixelSize(R.dimen.container_bounds_inset); + mContainerBoundsInset = getResources().getDimensionPixelSize( + R.dimen.container_bounds_inset); + mScrubberHeight = getResources().getDimensionPixelSize(R.dimen.scrubber_height); } @Override @@ -63,8 +77,12 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab updateBackgroundAndPaddings(); } - protected void setHasSearchBar() { - mHasSearchBar = true; + public void setHasSearchBar(boolean hasSearchBar) { + mHasSearchBar = hasSearchBar; + } + + public boolean hasSearchBar() { + return mHasSearchBar; } /** @@ -87,10 +105,57 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab }); } + public final void setScroller() { + Context context = getContext(); + mUseScroller = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_USE_SCROLLER, + R.bool.preferences_interface_use_scroller_default); + mUseScrubber = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_USE_HORIZONTAL_SCRUBBER, + R.bool.preferences_interface_use_horizontal_scrubber_default); + + if (mUseScroller && mUseScrubber) { + ViewStub stub = (ViewStub) findViewById(R.id.scrubber_container_stub); + mScrubberContainerView = stub.inflate(); + if (mScrubberContainerView == null) { + throw new IllegalStateException( + "Layout must contain an id: R.id.scrubber_container"); + } + mScrubber = (BaseRecyclerViewScrubber) + mScrubberContainerView.findViewById(R.id.base_scrubber); + BaseRecyclerView recyclerView = getRecyclerView(); + if (recyclerView != null) { + mScrubber.setRecycler(recyclerView); + mScrubber + .setScrubberIndicator((TextView) mScrubberContainerView + .findViewById(R.id.scrubberIndicator)); + mScrubber.updateSections(); + } + } else { + removeView(mScrubberContainerView); + BaseRecyclerView recyclerView = getRecyclerView(); + if (recyclerView != null) { + recyclerView.setUseScrollbar(mUseScroller); + } + } + } + + public final boolean useScroller() { + return mUseScroller; + } + + public final boolean useScrubber() { + return mUseScrubber; + } + + protected void updateBackgroundAndPaddings() { + updateBackgroundAndPaddings(false); + } + /** * Update the backgrounds and padding in response to a change in the bounds or insets. */ - protected void updateBackgroundAndPaddings() { + protected void updateBackgroundAndPaddings(boolean force) { Rect padding; Rect searchBarBounds = new Rect(); if (!isValidSearchBarBounds(mFixedSearchBarBounds)) { @@ -119,7 +184,8 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab // If either the computed container padding has changed, or the computed search bar bounds // has changed, then notify the container - if (!padding.equals(mContentPadding) || !searchBarBounds.equals(mSearchBarBounds)) { + if (force || !padding.equals(mContentPadding) || + !searchBarBounds.equals(mSearchBarBounds)) { mContentPadding.set(padding); mContentBounds.set(padding.left, padding.top, getMeasuredWidth() - padding.right, @@ -135,6 +201,11 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab protected abstract void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding); /** + * This might be null if the container doesn't have a recycler. + */ + protected abstract BaseRecyclerView getRecyclerView(); + + /** * Returns whether the search bar bounds we got are considered valid. */ private boolean isValidSearchBarBounds(Rect searchBarBounds) { diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java index f0d8b3b3d..d89c92270 100644 --- a/src/com/android/launcher3/BaseRecyclerView.java +++ b/src/com/android/launcher3/BaseRecyclerView.java @@ -57,6 +57,7 @@ public abstract class BaseRecyclerView extends RecyclerView } protected BaseRecyclerViewFastScrollBar mScrollbar; + protected boolean mUseScrollbar = false; private int mDownX; private int mDownY; @@ -74,10 +75,9 @@ public abstract class BaseRecyclerView extends RecyclerView public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP; - mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources()); ScrollListener listener = new ScrollListener(); - setOnScrollListener(listener); + addOnScrollListener(listener); } private class ScrollListener extends OnScrollListener { @@ -93,12 +93,16 @@ public abstract class BaseRecyclerView extends RecyclerView // initiate that here if the recycler view scroll state is not // RecyclerView.SCROLL_STATE_IDLE. - onUpdateScrollbar(dy); + if (mUseScrollbar) { + onUpdateScrollbar(dy); + } } } public void reset() { - mScrollbar.reattachThumbToScroll(); + if (mUseScrollbar) { + mScrollbar.reattachThumbToScroll(); + } } @Override @@ -137,19 +141,28 @@ public abstract class BaseRecyclerView extends RecyclerView if (shouldStopScroll(ev)) { stopScroll(); } - mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + if (mScrollbar != null) { + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + } break; case MotionEvent.ACTION_MOVE: mLastY = y; - mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + if (mScrollbar != null) { + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: onFastScrollCompleted(); - mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + if (mScrollbar != null) { + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + } break; } - return mScrollbar.isDraggingThumb(); + if (mUseScrollbar) { + return mScrollbar.isDraggingThumb(); + } + return false; } public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { @@ -183,7 +196,10 @@ public abstract class BaseRecyclerView extends RecyclerView * Returns the scroll bar width when the user is scrolling. */ public int getMaxScrollbarWidth() { - return mScrollbar.getThumbMaxWidth(); + if (mUseScrollbar) { + return mScrollbar.getThumbMaxWidth(); + } + return 0; } /** @@ -204,9 +220,12 @@ public abstract class BaseRecyclerView extends RecyclerView * AvailableScrollBarHeight = Total height of the visible view - thumb height */ protected int getAvailableScrollBarHeight() { - int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; - int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight(); - return availableScrollBarHeight; + if (mUseScrollbar) { + int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; + int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight(); + return availableScrollBarHeight; + } + return 0; } /** @@ -223,11 +242,23 @@ public abstract class BaseRecyclerView extends RecyclerView return defaultInactiveThumbColor; } + public void setUseScrollbar(boolean useScrollbar) { + mUseScrollbar = useScrollbar; + if (useScrollbar) { + mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources()); + } else { + mScrollbar = null; + } + invalidate(); + } + @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); - onUpdateScrollbar(0); - mScrollbar.draw(canvas); + if (mUseScrollbar) { + onUpdateScrollbar(0); + mScrollbar.draw(canvas); + } } /** @@ -241,6 +272,9 @@ public abstract class BaseRecyclerView extends RecyclerView */ protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, int rowCount) { + if (!mUseScrollbar) { + return; + } // Only show the scrollbar if there is height to be scrolled int availableScrollBarHeight = getAvailableScrollBarHeight(); int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight); @@ -252,8 +286,7 @@ public abstract class BaseRecyclerView extends RecyclerView // Calculate the current scroll position, the scrollY of the recycler view accounts for the // view padding, while the scrollBarY is drawn right up to the background padding (ignoring // padding) - int scrollY = getPaddingTop() + - (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; + int scrollY = getCurrentScroll(scrollPosState); int scrollBarY = mBackgroundPadding.top + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); @@ -268,11 +301,28 @@ public abstract class BaseRecyclerView extends RecyclerView } /** + * @param scrollPosState current state of view scrolling. + * @return the vertical scroll position + */ + protected int getCurrentScroll(ScrollPositionState scrollPosState) { + return getPaddingTop() + (scrollPosState.rowIndex * scrollPosState.rowHeight) - + scrollPosState.rowTopOffset; + } + + /** * Maps the touch (from 0..1) to the adapter position that should be visible. * <p>Override in each subclass of this base class. */ public abstract String scrollToPositionAtProgress(float touchFraction); + public abstract String scrollToSection(String sectionName); + + public abstract String[] getSectionNames(); + + public void setFastScrollDragging(boolean dragging) {} + + public void setPreviousSectionFastScrollFocused() {} + /** * Updates the bounds for the scrollbar. * <p>Override in each subclass of this base class. diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java index fcee7e8dd..006cfccef 100644 --- a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java @@ -18,6 +18,7 @@ package com.android.launcher3; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.content.res.Resources; import android.graphics.Canvas; @@ -27,8 +28,11 @@ import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.view.MotionEvent; +import android.view.View; import android.view.ViewConfiguration; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; import com.android.launcher3.util.Thunk; /** @@ -36,8 +40,128 @@ import com.android.launcher3.util.Thunk; */ public class BaseRecyclerViewFastScrollBar { - public interface FastScrollFocusableView { + public interface FastScrollFocusable { + int FAST_SCROLL_FOCUS_DIMMABLE = 1; + int FAST_SCROLL_FOCUS_SCALABLE = 2; + void setFastScrollFocused(boolean focused, boolean animated); + void setFastScrollDimmed(boolean dimmed, boolean animated); + } + + /** + * Helper class to apply fast scroll focus functionality to any view. + */ + public static class FastScrollFocusApplicator implements FastScrollFocusable { + private static final int FAST_SCROLL_FOCUS_FADE_IN_DURATION = 175; + private static final int FAST_SCROLL_FOCUS_FADE_OUT_DURATION = 125; + private static final float FAST_SCROLL_FOCUS_MAX_SCALE = 1.15f; + + private final View mView; + private final int mFastScrollMode; + + private ObjectAnimator mFastScrollFocusAnimator; + private ObjectAnimator mFastScrollDimAnimator; + private boolean mFastScrollFocused; + private boolean mFastScrollDimmed; + + public static void createApplicator(final View v, int mode) { + FastScrollFocusApplicator applicator = new FastScrollFocusApplicator(v, mode); + v.setTag(R.id.fast_scroll_focus_applicator_tag, applicator); + } + + public static void setFastScrollFocused(final View v, boolean focused, boolean animated) { + FastScrollFocusable focusable = getFromView(v); + if (focusable == null) return; + + focusable.setFastScrollFocused(focused, animated); + } + + public static void setFastScrollDimmed(final View v, boolean dimmed, boolean animated) { + FastScrollFocusable focusable = getFromView(v); + if (focusable == null) return; + + focusable.setFastScrollDimmed(dimmed, animated); + } + + private static FastScrollFocusable getFromView(final View v) { + Object tag = v.getTag(R.id.fast_scroll_focus_applicator_tag); + if (tag != null) { + return (FastScrollFocusApplicator) tag; + } + return null; + } + + private FastScrollFocusApplicator(final View v, final int mode) { + mView = v; + mFastScrollMode = mode & ~FAST_SCROLL_FOCUS_SCALABLE; // Globally disable scaling. + } + + public void setFastScrollFocused(boolean focused, boolean animated) { + if ((mFastScrollMode & FAST_SCROLL_FOCUS_SCALABLE) == 0) { + return; + } + + if (mFastScrollFocused != focused) { + mFastScrollFocused = focused; + + if (animated) { + // Clean up the previous focus animator + if (mFastScrollFocusAnimator != null) { + mFastScrollFocusAnimator.cancel(); + } + + // Setup animator for bi-directional scaling. + float value = focused ? FAST_SCROLL_FOCUS_MAX_SCALE : 1f; + PropertyValuesHolder pvhScaleX = + PropertyValuesHolder.ofFloat(View.SCALE_X, value); + PropertyValuesHolder pvhScaleY = + PropertyValuesHolder.ofFloat(View.SCALE_Y, value); + mFastScrollFocusAnimator = ObjectAnimator.ofPropertyValuesHolder(mView, + pvhScaleX, pvhScaleY); + + if (focused) { + mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator()); + } else { + mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator()); + } + mFastScrollFocusAnimator.setDuration(focused ? + FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION); + mFastScrollFocusAnimator.start(); + } + } + + // Let the view do any additional operations if it wants. + if (mView instanceof FastScrollFocusable) { + ((FastScrollFocusable) mView).setFastScrollFocused(focused, animated); + } + } + + public void setFastScrollDimmed(boolean dimmed, boolean animated) { + if ((mFastScrollMode & FAST_SCROLL_FOCUS_DIMMABLE) == 0) { + return; + } + + if (!animated) { + mFastScrollDimmed = dimmed; + mView.setAlpha(dimmed ? 0.4f : 1f); + } else if (mFastScrollDimmed != dimmed) { + mFastScrollDimmed = dimmed; + + // Clean up the previous dim animator + if (mFastScrollDimAnimator != null) { + mFastScrollDimAnimator.cancel(); + } + mFastScrollDimAnimator = ObjectAnimator.ofFloat(mView, View.ALPHA, dimmed ? 0.4f : 1f); + mFastScrollDimAnimator.setDuration(dimmed ? + FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION); + mFastScrollDimAnimator.start(); + } + + // Let the view do any additional operations if it wants. + if (mView instanceof FastScrollFocusable) { + ((FastScrollFocusable) mView).setFastScrollDimmed(dimmed, animated); + } + } } private final static int MAX_TRACK_ALPHA = 30; @@ -193,6 +317,7 @@ public class BaseRecyclerViewFastScrollBar { Math.abs(y - downY) > config.getScaledTouchSlop()) { mRv.getParent().requestDisallowInterceptTouchEvent(true); mIsDragging = true; + mRv.setFastScrollDragging(mIsDragging); if (mCanThumbDetach) { mIsThumbDetached = true; } @@ -220,6 +345,7 @@ public class BaseRecyclerViewFastScrollBar { mIgnoreDragGesture = false; if (mIsDragging) { mIsDragging = false; + mRv.setFastScrollDragging(mIsDragging); mPopup.animateVisibility(false); animateScrollbar(false); } diff --git a/src/com/android/launcher3/BaseRecyclerViewScrubber.java b/src/com/android/launcher3/BaseRecyclerViewScrubber.java new file mode 100644 index 000000000..7795de207 --- /dev/null +++ b/src/com/android/launcher3/BaseRecyclerViewScrubber.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.graphics.Color; +import android.graphics.PointF; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Message; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.LinearSmoothScroller; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import java.lang.IllegalArgumentException; +import java.util.ArrayList; +import java.util.Collections; + +/** + * BaseRecyclerViewScrubber + * <pre> + * This is the scrubber at the bottom of a BaseRecyclerView + * </pre> + * + * @see {@link LinearLayout} + */ +public class BaseRecyclerViewScrubber extends LinearLayout { + private BaseRecyclerView mBaseRecyclerView; + private TextView mScrubberIndicator; + private SeekBar mSeekBar; + private AutoExpandTextView mScrubberText; + private SectionContainer mSectionContainer; + private ScrubberAnimationState mScrubberAnimationState; + private Drawable mTransparentDrawable; + private boolean mIsRtl; + + private static final int MSG_SET_TARGET = 1000; + private static final int MSG_ANIMATE_PICK = MSG_SET_TARGET + 1; + + /** + * UiHandler + * <pre> + * Using a handler for sending signals to perform certain actions. The reason for + * using this is to be able to remove and replace a signal if signals are being + * sent too fast (e.g. user scrubbing like crazy). This allows the touch loop to + * complete then later run the animations in their own loops. + * </pre> + */ + private class UiHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SET_TARGET: + int adapterIndex = msg.arg1; + performSetTarget(adapterIndex); + + break; + case MSG_ANIMATE_PICK: + int index = msg.arg1; + int width = msg.arg2; + int lastIndex = (Integer)msg.obj; + performAnimatePickMessage(index, width, lastIndex); + break; + default: + super.handleMessage(msg); + } + } + + /** + * Overidden to remove identical calls if they are called subsequently fast enough. + * + * This is the final point that is public in the call chain. Other calls to sendMessageXXX + * will eventually call this function which calls "enqueueMessage" which is private. + * + * @param msg {@link Message} + * @param uptimeMillis {@link Long} + * + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + @Override + public boolean sendMessageAtTime(Message msg, long uptimeMillis) throws + IllegalArgumentException { + if (msg == null) { + throw new IllegalArgumentException("'msg' cannot be null!"); + } + if (hasMessages(msg.what)) { + removeMessages(msg.what); + } + return super.sendMessageAtTime(msg, uptimeMillis); + } + + } + private Handler mUiHandler = new UiHandler(); + private void sendSetTargetMessage(int adapterIndex) { + Message msg = mUiHandler.obtainMessage(MSG_SET_TARGET); + msg.what = MSG_SET_TARGET; + msg.arg1 = adapterIndex; + mUiHandler.sendMessage(msg); + } + private void performSetTarget(int adapterIndex) { + mBaseRecyclerView.scrollToSection(mSectionContainer.getSectionName(adapterIndex, mIsRtl)); + } + private void sendAnimatePickMessage(int index, int width, int lastIndex) { + Message msg = mUiHandler.obtainMessage(MSG_ANIMATE_PICK); + msg.what = MSG_ANIMATE_PICK; + msg.arg1 = index; + msg.arg2 = width; + msg.obj = lastIndex; + mUiHandler.sendMessage(msg); + } + private void performAnimatePickMessage(int index, int width, int lastIndex) { + if (mScrubberIndicator != null) { + // get the index based on the direction the user is scrolling + int directionalIndex = mSectionContainer.getDirectionalIndex(lastIndex, index); + String sectionText = mSectionContainer.getSectionName(directionalIndex, mIsRtl); + float translateX = (index * width) / (float) mSectionContainer.size(); + // if we are showing letters, grab the position based on the text view + if (mSectionContainer.showLetters()) { + translateX = mScrubberText.getPositionOfSection(index); + } + // center the x position + translateX -= mScrubberIndicator.getMeasuredWidth() / 2; + if (mIsRtl) { + translateX = -translateX; + } + mScrubberIndicator.setTranslationX(translateX); + mScrubberIndicator.setText(sectionText); + } + } + + /** + * Constructor + * + * @param context {@link Context} + * @param attrs {@link AttributeSet} + */ + public BaseRecyclerViewScrubber(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** + * Constructor + * + * @param context {@link Context} + */ + public BaseRecyclerViewScrubber(Context context) { + super(context); + init(context); + } + + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + super.onRtlPropertiesChanged(layoutDirection); + mIsRtl = Utilities.isRtl(getResources()); + updateSections(); + } + + /** + * Simple container class that tries to abstract out the knowledge of complex sections vs + * simple string sections + */ + private static class SectionContainer { + private BaseRecyclerViewScrubberSection. + RtlIndexArrayList<BaseRecyclerViewScrubberSection> mSections; + private String[] mSectionNames; + private final boolean mIsRtl; + + public SectionContainer(String[] sections, boolean isRtl) { + mIsRtl = isRtl; + mSections = BaseRecyclerViewScrubberSection.createSections(sections, isRtl); + mSectionNames = sections; + if (isRtl && mSections != null) { + final int N = mSectionNames.length; + for(int i = 0; i < N / 2; i++) { + String temp = mSectionNames[i]; + mSectionNames[i] = mSectionNames[N - i - 1]; + mSectionNames[N - i - 1] = temp; + } + Collections.reverse(mSections); + } + } + + public int size() { + return showLetters() ? mSections.size() : mSectionNames.length; + } + + public String getSectionName(int idx, boolean isRtl) { + if (size() == 0) { + return null; + } + return showLetters() ? mSections.get(idx, isRtl).getText() : mSectionNames[idx]; + } + + /** + * Because the list section headers is not necessarily the same size as the scrubber + * letters, we need to map from the larger list to the smaller list. + * In the case that curIdx is not highlighted, it will use the directional index to + * determine the adapter index + * @return the mSectionNames index (aka the underlying adapter index). + */ + public int getAdapterIndex(int prevIdx, int curIdx) { + if (!showLetters() || size() == 0) { + return curIdx; + } + + // because we have some unhighlighted letters, we need to first get the directional + // index before getting the adapter index + return mSections.get(getDirectionalIndex(prevIdx, curIdx), mIsRtl).getAdapterIndex(); + } + + /** + * Given the direction the user is scrolling in, return the closest index which is a + * highlighted index + */ + public int getDirectionalIndex(int prevIdx, int curIdx) { + if (!showLetters() || size() == 0 || mSections.get(curIdx, mIsRtl).getHighlight()) { + return curIdx; + } + + if (prevIdx < curIdx) { + if (mIsRtl) { + return mSections.get(curIdx).getPreviousIndex(); + } else { + return mSections.get(curIdx).getNextIndex(); + } + } else { + if (mIsRtl) { + return mSections.get(curIdx).getNextIndex(); + } else { + return mSections.get(curIdx).getPreviousIndex(); + } + + } + } + + /** + * @return true if the scrubber is showing characters as opposed to a line + */ + public boolean showLetters() { + return mSections != null; + } + + /** + * Initializes the scrubber text with the proper characters + */ + public void initializeScrubberText(AutoExpandTextView scrubberText) { + scrubberText.setSections(BaseRecyclerViewScrubberSection.getHighlightText(mSections)); + } + } + + public void updateSections() { + if (mBaseRecyclerView != null) { + mSectionContainer = new SectionContainer(mBaseRecyclerView.getSectionNames(), mIsRtl); + mSectionContainer.initializeScrubberText(mScrubberText); + mSeekBar.setMax(mSectionContainer.size() - 1); + + // show a white line if there are no letters, otherwise show transparent + Drawable d = mSectionContainer.showLetters() ? mTransparentDrawable + : getContext().getResources().getDrawable(R.drawable.seek_back); + ((ViewGroup) mSeekBar.getParent()).setBackground(d); + } + } + + public void setRecycler(BaseRecyclerView baseRecyclerView) { + mBaseRecyclerView = baseRecyclerView; + } + + public void setScrubberIndicator(TextView scrubberIndicator) { + mScrubberIndicator = scrubberIndicator; + } + + private boolean isReady() { + return mBaseRecyclerView != null && + mSectionContainer != null; + } + + private void init(Context context) { + mIsRtl = Utilities.isRtl(context.getResources()); + LayoutInflater.from(context).inflate(R.layout.scrub_layout, this); + mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT); + mScrubberAnimationState = new ScrubberAnimationState(); + mSeekBar = (SeekBar) findViewById(R.id.scrubber); + mScrubberText = (AutoExpandTextView) findViewById(R.id.scrubberText); + mSeekBar.setOnSeekBarChangeListener(mScrubberAnimationState); + } + + /** + * Handles the animations of the scrubber indicator + */ + private class ScrubberAnimationState implements SeekBar.OnSeekBarChangeListener { + private static final long SCRUBBER_DISPLAY_DURATION_IN = 60; + private static final long SCRUBBER_DISPLAY_DURATION_OUT = 150; + private static final long SCRUBBER_DISPLAY_DELAY_IN = 0; + private static final long SCRUBBER_DISPLAY_DELAY_OUT = 200; + private static final float SCRUBBER_SCALE_START = 0f; + private static final float SCRUBBER_SCALE_END = 1f; + private static final float SCRUBBER_ALPHA_START = 0f; + private static final float SCRUBBER_ALPHA_END = 1f; + + private boolean mTouchingTrack = false; + private boolean mAnimatingIn = false; + private int mLastIndex = -1; + + private void touchTrack(boolean touching) { + mTouchingTrack = touching; + + if (mScrubberIndicator != null) { + if (mTouchingTrack) { + animateIn(); + } else if (!mAnimatingIn) { // finish animating in before animating out + animateOut(); + } + + mBaseRecyclerView.setFastScrollDragging(mTouchingTrack); + if (mTouchingTrack) { + mBaseRecyclerView.setPreviousSectionFastScrollFocused(); + } + + } + } + + private void animateIn() { + if (mScrubberIndicator == null) { + return; + } + // start from a scratch position when animating in + mScrubberIndicator.animate().cancel(); + mScrubberIndicator.setPivotX(mScrubberIndicator.getMeasuredWidth() / 2); + mScrubberIndicator.setPivotY(mScrubberIndicator.getMeasuredHeight() * 0.9f); + mScrubberIndicator.setAlpha(SCRUBBER_ALPHA_START); + mScrubberIndicator.setScaleX(SCRUBBER_SCALE_START); + mScrubberIndicator.setScaleY(SCRUBBER_SCALE_START); + mScrubberIndicator.setVisibility(View.VISIBLE); + mAnimatingIn = true; + + mScrubberIndicator.animate() + .alpha(SCRUBBER_ALPHA_END) + .scaleX(SCRUBBER_SCALE_END) + .scaleY(SCRUBBER_SCALE_END) + .setStartDelay(SCRUBBER_DISPLAY_DELAY_IN) + .setDuration(SCRUBBER_DISPLAY_DURATION_IN) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnimatingIn = false; + // if the user has stopped touching the seekbar, animate back out + if (!mTouchingTrack) { + animateOut(); + } + } + }).start(); + } + + private void animateOut() { + if (mScrubberIndicator == null) { + return; + } + mScrubberIndicator.animate() + .alpha(SCRUBBER_ALPHA_START) + .scaleX(SCRUBBER_SCALE_START) + .scaleY(SCRUBBER_SCALE_START) + .setStartDelay(SCRUBBER_DISPLAY_DELAY_OUT) + .setDuration(SCRUBBER_DISPLAY_DURATION_OUT) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mScrubberIndicator.setVisibility(View.INVISIBLE); + } + }); + } + + @Override + public void onProgressChanged(SeekBar seekBar, int index, boolean fromUser) { + if (!isReady()) { + return; + } + progressChanged(seekBar, index, fromUser); + } + + private void progressChanged(SeekBar seekBar, int index, boolean fromUser) { + if (!fromUser) { + return; + } + + sendAnimatePickMessage(index, seekBar.getWidth(), mLastIndex); + + // get the index of the underlying list + int adapterIndex = mSectionContainer.getDirectionalIndex(mLastIndex, index); + // Post set target index on queue to get processed by Looper later + sendSetTargetMessage(adapterIndex); + + mLastIndex = index; + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + touchTrack(true); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + touchTrack(false); + } + } +} diff --git a/src/com/android/launcher3/BaseRecyclerViewScrubberSection.java b/src/com/android/launcher3/BaseRecyclerViewScrubberSection.java new file mode 100644 index 000000000..1d17ea887 --- /dev/null +++ b/src/com/android/launcher3/BaseRecyclerViewScrubberSection.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2015 The CyanogenMod 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; + +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; + +public class BaseRecyclerViewScrubberSection { + private static final String TAG = "BRVScrubberSections"; + private static final String ALPHA_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final int MAX_NUMBER_CUSTOM_SECTIONS = 8; + private static final int MAX_SECTIONS = ALPHA_LETTERS.length() + MAX_NUMBER_CUSTOM_SECTIONS; + public static final int INVALID_INDEX = -1; + + private AutoExpandTextView.HighlightedText mHighlightedText; + private int mPreviousValidIndex; + private int mNextValidIndex; + private int mAdapterIndex; + + public BaseRecyclerViewScrubberSection(String text, boolean highlight, int idx) { + mHighlightedText = new AutoExpandTextView.HighlightedText(text, highlight); + mAdapterIndex = idx; + mPreviousValidIndex = mNextValidIndex = idx; + } + + public boolean getHighlight() { + return mHighlightedText.mHighlight; + } + + public String getText() { + return mHighlightedText.mText; + } + + public int getPreviousIndex() { + return mPreviousValidIndex; + } + + public int getNextIndex() { + return mNextValidIndex; + } + + public int getAdapterIndex() { + return mAdapterIndex; + } + + private static int + getFirstValidIndex(RtlIndexArrayList<BaseRecyclerViewScrubberSection> sections, + boolean isRtl) { + for (int i = 0; i < sections.size(); i++) { + if (sections.get(i, isRtl).getHighlight()) { + return i; + } + } + + return INVALID_INDEX; + } + + private static void createIndices(RtlIndexArrayList<BaseRecyclerViewScrubberSection> sections, + boolean isRtl) { + if (sections == null || sections.size() == 0) { + return; + } + + // walk forwards and fill out the previous valid index based on the previous highlight + int currentIdx = getFirstValidIndex(sections, isRtl); + for (int i = 0; i < sections.size(); i++) { + if (sections.get(i, isRtl).getHighlight()) { + currentIdx = i; + } + + sections.get(i, isRtl).mPreviousValidIndex = currentIdx; + } + + // currentIdx should be now on the last valid index so walk back and fill the other way + for (int i = sections.size() - 1; i >= 0; i--) { + if (sections.get(i, isRtl).getHighlight()) { + currentIdx = i; + } + + sections.get(i, isRtl).mNextValidIndex = currentIdx; + } + } + + public static ArrayList<AutoExpandTextView.HighlightedText> getHighlightText( + RtlIndexArrayList<BaseRecyclerViewScrubberSection> sections) { + if (sections == null) { + return null; + } + + ArrayList<AutoExpandTextView.HighlightedText> highlights = new ArrayList<>(sections.size()); + for (BaseRecyclerViewScrubberSection section : sections) { + highlights.add(section.mHighlightedText); + } + + return highlights; + } + + private static void addAlphaLetters(RtlIndexArrayList<BaseRecyclerViewScrubberSection> sections, + HashMap<Integer, Integer> foundAlphaLetters) { + for (int i = 0; i < ALPHA_LETTERS.length(); i++) { + boolean highlighted = foundAlphaLetters.containsKey(i); + int index = highlighted + ? foundAlphaLetters.get(i) : BaseRecyclerViewScrubberSection.INVALID_INDEX; + + sections.add(new BaseRecyclerViewScrubberSection(ALPHA_LETTERS.substring(i, i + 1), + highlighted, index)); + } + } + + /** + * Takes the sections and runs some checks to see if we can create a valid + * appDrawerScrubberSection out of it. This list will contain the original header list plus + * fill out the remaining sections based on the ALPHA_LETTERS. It will then determine which + * ones to highlight as well as what letters to highlight when scrolling over the + * grayed out sections + * @param sectionNames list of sectionName Strings + * @return the list of scrubber sections + */ + public static RtlIndexArrayList<BaseRecyclerViewScrubberSection> + createSections(String[] sectionNames, boolean isRtl) { + // check if we have a valid header section + if (!validSectionNameList(sectionNames)) { + return null; + } + + // this will track the mapping of ALPHA_LETTERS index to the headers index + HashMap<Integer, Integer> foundAlphaLetters = new HashMap<>(); + RtlIndexArrayList<BaseRecyclerViewScrubberSection> sections = + new RtlIndexArrayList<>(sectionNames.length); + boolean inAlphaLetterSection = false; + + for (int i = 0; i < sectionNames.length; i++) { + int alphaLetterIndex = TextUtils.isEmpty(sectionNames[i]) + ? -1 : ALPHA_LETTERS.indexOf(sectionNames[i]); + + // if we found an ALPHA_LETTERS store that in foundAlphaLetters and continue + if (alphaLetterIndex >= 0) { + foundAlphaLetters.put(alphaLetterIndex, i); + inAlphaLetterSection = true; + } else { + // if we are exiting the ALPHA_LETTERS section, add it here + if (inAlphaLetterSection) { + addAlphaLetters(sections, foundAlphaLetters); + inAlphaLetterSection = false; + } + + // add the custom header + sections.add(new BaseRecyclerViewScrubberSection(sectionNames[i], true, i)); + } + } + + // if the last section are the alpha letters, then add it + if (inAlphaLetterSection) { + addAlphaLetters(sections, foundAlphaLetters); + } + + // create the forward and backwards indices for scrolling over the grayed out sections + BaseRecyclerViewScrubberSection.createIndices(sections, isRtl); + + return sections; + } + + /** + * Walk through the sectionNames and check for a few things: + * 1) No more than MAX_NUMBER_CUSTOM_SECTIONS sectionNames exist in the sectionNames list or no more + * than MAX_SECTIONS sectionNames exist in the list + * 2) the headers that fall in the ALPHA_LETTERS category are in the same order as ALPHA_LETTERS + * 3) There are no sectionNames that exceed length of 1 + * 4) The alpha letter sectionName is together and not separated by other things + */ + private static boolean validSectionNameList(String[] sectionNames) { + int numCustomSections = 0; + int previousAlphaIndex = -1; + boolean foundAlphaSections = false; + + for (String s : sectionNames) { + if (TextUtils.isEmpty(s)) { + numCustomSections++; + continue; + } + + if (s.length() > 1) { + Log.w(TAG, "Found section " + s + " with length: " + s.length()); + return false; + } + + int alphaIndex = ALPHA_LETTERS.indexOf(s); + if (alphaIndex >= 0) { + if (previousAlphaIndex != -1) { + // if the previous alpha index is >= alphaIndex then it is in the wrong order + if (previousAlphaIndex >= alphaIndex) { + Log.w(TAG, "Found letter index " + previousAlphaIndex + + " which is greater than " + alphaIndex); + return false; + } + } + + // if we've found headers previously and the index is -1 that means the alpha + // letters are separated out into two sections so return false + if (foundAlphaSections && previousAlphaIndex == -1) { + Log.w(TAG, "Found alpha letters twice"); + return false; + } + + previousAlphaIndex = alphaIndex; + foundAlphaSections = true; + } else { + numCustomSections++; + previousAlphaIndex = -1; + } + } + + final int listSize = foundAlphaSections + ? numCustomSections + ALPHA_LETTERS.length() + : numCustomSections; + + // if one of these conditions are satisfied, then return true + if (numCustomSections <= MAX_NUMBER_CUSTOM_SECTIONS || listSize <= MAX_SECTIONS) { + return true; + } + + if (numCustomSections > MAX_NUMBER_CUSTOM_SECTIONS) { + Log.w(TAG, "Found " + numCustomSections + "# custom sections when " + + MAX_NUMBER_CUSTOM_SECTIONS + " is allowed!"); + } else if (listSize > MAX_SECTIONS) { + Log.w(TAG, "Found " + listSize + " sections when " + + MAX_SECTIONS + " is allowed!"); + } + + return false; + } + + public static class RtlIndexArrayList<T> extends ArrayList<T> { + + public RtlIndexArrayList(int capacity) { + super(capacity); + } + + public T get(int index, boolean isRtl) { + if (isRtl) { + index = size() - 1 - index; + } + return super.get(index); + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 507087824..facafd354 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -16,7 +16,6 @@ package com.android.launcher3; -import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; @@ -37,8 +36,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; import android.widget.TextView; import com.android.launcher3.IconCache.IconLoadRequest; @@ -50,7 +47,7 @@ import com.android.launcher3.model.PackageItemInfo; * too aggressive. */ public class BubbleTextView extends TextView - implements BaseRecyclerViewFastScrollBar.FastScrollFocusableView { + implements BaseRecyclerViewFastScrollBar.FastScrollFocusable { private static SparseArray<Theme> sPreloaderThemes = new SparseArray<Theme>(2); @@ -63,12 +60,9 @@ public class BubbleTextView extends TextView private static final int DISPLAY_WORKSPACE = 0; private static final int DISPLAY_ALL_APPS = 1; - private static final float FAST_SCROLL_FOCUS_MAX_SCALE = 1.15f; private static final int FAST_SCROLL_FOCUS_MODE_NONE = 0; private static final int FAST_SCROLL_FOCUS_MODE_SCALE_ICON = 1; private static final int FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG = 2; - private static final int FAST_SCROLL_FOCUS_FADE_IN_DURATION = 175; - private static final int FAST_SCROLL_FOCUS_FADE_OUT_DURATION = 125; private final Launcher mLauncher; private Drawable mIcon; @@ -93,10 +87,8 @@ public class BubbleTextView extends TextView private boolean mIgnorePressedStateChange; private boolean mDisableRelayout = false; - private ObjectAnimator mFastScrollFocusAnimator; private Paint mFastScrollFocusBgPaint; private float mFastScrollFocusFraction; - private boolean mFastScrollFocused; private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON; private IconLoadRequest mIconLoadRequest; @@ -167,10 +159,21 @@ public class BubbleTextView extends TextView public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, boolean promiseStateChanged) { - Bitmap b = info.getIcon(iconCache); + Drawable iconDrawable; + if (info.customDrawable != null) { + iconDrawable = info.customDrawable; + } else { + Bitmap b = info.getIcon(iconCache); + + if (b.getWidth() > mIconSize || b.getHeight() > mIconSize) { + b = Bitmap.createScaledBitmap(b, mIconSize, mIconSize, false); + info.setIcon(b); + info.updateIcon(iconCache); + } - FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b); - iconDrawable.setGhostModeEnabled(info.isDisabled != 0); + iconDrawable = mLauncher.createIconDrawable(b); + ((FastBitmapDrawable) iconDrawable).setGhostModeEnabled(info.isDisabled != 0); + } setIcon(iconDrawable, mIconSize); if (info.contentDescription != null) { @@ -185,7 +188,13 @@ public class BubbleTextView extends TextView } public void applyFromApplicationInfo(AppInfo info) { - setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize); + Drawable iconDrawable; + if (info.customDrawable != null) { + iconDrawable = info.customDrawable; + } else { + iconDrawable = mLauncher.createIconDrawable(info.iconBitmap); + } + setIcon(iconDrawable, mIconSize); setText(info.title); if (info.contentDescription != null) { setContentDescription(info.contentDescription); @@ -558,6 +567,11 @@ public class BubbleTextView extends TextView * Verifies that the current icon is high-res otherwise posts a request to load the icon. */ public void verifyHighRes() { + // Custom drawables cannot be verified. + if (getTag() instanceof ItemInfo && ((ItemInfo) getTag()).customDrawable != null) { + return; + } + if (mIconLoadRequest != null) { mIconLoadRequest.cancel(); mIconLoadRequest = null; @@ -583,51 +597,22 @@ public class BubbleTextView extends TextView } } - // Setters & getters for the animation - public void setFastScrollFocus(float fraction) { - mFastScrollFocusFraction = fraction; - if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_SCALE_ICON) { - setScaleX(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); - setScaleY(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); - } else { - invalidate(); - } - } - - public float getFastScrollFocus() { - return mFastScrollFocusFraction; - } - @Override public void setFastScrollFocused(final boolean focused, boolean animated) { if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) { return; } - if (mFastScrollFocused != focused) { - mFastScrollFocused = focused; - - if (animated) { - // Clean up the previous focus animator - if (mFastScrollFocusAnimator != null) { - mFastScrollFocusAnimator.cancel(); - } - mFastScrollFocusAnimator = ObjectAnimator.ofFloat(this, "fastScrollFocus", - focused ? 1f : 0f); - if (focused) { - mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator()); - } else { - mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator()); - } - mFastScrollFocusAnimator.setDuration(focused ? - FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION); - mFastScrollFocusAnimator.start(); - } else { - mFastScrollFocusFraction = focused ? 1f : 0f; - } + if (!animated) { + mFastScrollFocusFraction = focused ? 1f : 0f; } } + @Override + public void setFastScrollDimmed(boolean dimmed, boolean animated) { + // No special functionality here. + } + /** * Interface to be implemented by the grand parent to allow click shadow effect. */ diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 84e2d49c2..10599fa5e 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -504,13 +504,15 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { // Draw inner ring d = FolderRingAnimator.sSharedInnerRingDrawable; - width = (int) (fra.getInnerRingSize() * getChildrenScale()); - height = width; - canvas.save(); - canvas.translate(centerX - width / 2, centerY - width / 2); - d.setBounds(0, 0, width, height); - d.draw(canvas); - canvas.restore(); + if (d != null) { + width = (int) (fra.getInnerRingSize() * getChildrenScale()); + height = width; + canvas.save(); + canvas.translate(centerX - width / 2, centerY - width / 2); + d.setBounds(0, 0, width, height); + d.draw(canvas); + canvas.restore(); + } } } @@ -613,9 +615,8 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { final LayoutParams lp = params; // Hotseat icons - remove text - if (child instanceof BubbleTextView) { - BubbleTextView bubbleChild = (BubbleTextView) child; - bubbleChild.setTextVisibility(!mIsHotseat); + if (mIsHotseat && child instanceof BubbleTextView) { + ((BubbleTextView) child).setTextVisibility(false); } child.setScaleX(getChildrenScale()); @@ -2223,7 +2224,7 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { mLauncher.getWorkspace().updateItemLocationsInDatabase(this); } - private void setUseTempCoords(boolean useTempCoords) { + public void setUseTempCoords(boolean useTempCoords) { int childCount = mShortcutsAndWidgets.getChildCount(); for (int i = 0; i < childCount; i++) { LayoutParams lp = (LayoutParams) mShortcutsAndWidgets.getChildAt(i).getLayoutParams(); diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java index 483c62249..bd1d84128 100644 --- a/src/com/android/launcher3/CheckLongPressHelper.java +++ b/src/com/android/launcher3/CheckLongPressHelper.java @@ -30,6 +30,8 @@ public class CheckLongPressHelper { class CheckForLongPress implements Runnable { public void run() { + if (!mView.isLongClickable()) return; + if ((mView.getParent() != null) && mView.hasWindowFocus() && !mHasPerformedLongPress) { boolean handled; diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index 9c8659c29..f51a43a91 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -74,8 +74,14 @@ public class DeleteDropTarget extends ButtonDropTarget { LauncherModel.deleteItemFromDatabase(launcher, item); } else if (item instanceof FolderInfo) { FolderInfo folder = (FolderInfo) item; - launcher.removeFolder(folder); - LauncherModel.deleteFolderContentsFromDatabase(launcher, folder); + + // Remote folder should not really be deleted. Let the manager handle it. + if (folder.isRemote()) { + launcher.getRemoteFolderManager().onFolderDeleted(); + } else { + launcher.removeFolder(folder); + LauncherModel.deleteFolderContentsFromDatabase(launcher, folder); + } } else if (item instanceof LauncherAppWidgetInfo) { final LauncherAppWidgetInfo widget = (LauncherAppWidgetInfo) item; diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 774594fe2..51c634d4f 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -29,12 +29,13 @@ import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; -import android.view.ViewGroup.MarginLayoutParams; import android.widget.FrameLayout; import android.widget.LinearLayout; +import com.android.launcher3.settings.SettingsProvider; -public class DeviceProfile { +import com.android.launcher3.allapps.AllAppsContainerView; +public class DeviceProfile { public final InvariantDeviceProfile inv; // Device properties @@ -94,8 +95,10 @@ public class DeviceProfile { public final int allAppsIconTextSizePx; // QSB + public boolean searchBarVisible; private int searchBarSpaceWidthPx; private int searchBarSpaceHeightPx; + private int defaultSearchBarSpaceHeightPx; public DeviceProfile(Context context, InvariantDeviceProfile inv, Point minSize, Point maxSize, @@ -156,8 +159,16 @@ public class DeviceProfile { } // Calculate the remaining vars - updateAvailableDimensions(dm, res); + updateAvailableDimensions(dm, res, isLandscape); computeAllAppsButtonSize(context); + + // Search Bar + searchBarVisible = isSearchBarEnabled(context); + searchBarSpaceWidthPx = Math.min(searchBarSpaceWidthPx, widthPx); + defaultSearchBarSpaceHeightPx = getSearchBarTopOffset() + + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); + searchBarSpaceHeightPx = 2 * edgeMarginPx + (searchBarVisible ? + defaultSearchBarSpaceHeightPx - getSearchBarTopOffset() : 3 * edgeMarginPx); } /** @@ -170,7 +181,7 @@ public class DeviceProfile { allAppsButtonVisualSize = (int) (hotseatIconSizePx * (1 - padding)); } - private void updateAvailableDimensions(DisplayMetrics dm, Resources res) { + private void updateAvailableDimensions(DisplayMetrics dm, Resources res, boolean isLandscape) { // Check to see if the icons fit in the new available height. If not, then we need to // shrink the icon size. float scale = 1f; @@ -181,6 +192,9 @@ public class DeviceProfile { // We only care about the top and bottom workspace padding, which is not affected by RTL. Rect workspacePadding = getWorkspacePadding(false /* isLayoutRtl */); int maxHeight = (availableHeightPx - workspacePadding.top - workspacePadding.bottom); + if (!isLandscape) { //Include the hotseat and search bar if portrait + maxHeight -= (hotseatBarHeightPx + searchBarSpaceHeightPx); + } if (usedHeight > maxHeight) { scale = maxHeight / usedHeight; drawablePadding = 0; @@ -198,8 +212,9 @@ public class DeviceProfile { // Search Bar searchBarSpaceWidthPx = Math.min(widthPx, res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width)); - searchBarSpaceHeightPx = getSearchBarTopOffset() + defaultSearchBarSpaceHeightPx = getSearchBarTopOffset() + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); + searchBarSpaceHeightPx = defaultSearchBarSpaceHeightPx; // Calculate the actual text height Paint textPaint = new Paint(); @@ -225,11 +240,13 @@ public class DeviceProfile { /** * @param recyclerViewWidth the available width of the AllAppsRecyclerView */ - public void updateAppsViewNumCols(Resources res, int recyclerViewWidth) { - int appsViewLeftMarginPx = - res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); - int allAppsCellWidthGap = - res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap); + public void updateAppsViewNumCols(Resources res, int recyclerViewWidth, int gridStrategy) { + int appsViewLeftMarginPx = gridStrategy == AllAppsContainerView.SECTION_STRATEGY_GRID ? + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) : + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections); + int allAppsCellWidthGap = gridStrategy == AllAppsContainerView.SECTION_STRATEGY_GRID ? + res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap) : + res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap_with_sections); int availableAppsWidthPx = (recyclerViewWidth > 0) ? recyclerViewWidth : availableWidthPx; int numAppsCols = (availableAppsWidthPx - appsViewLeftMarginPx) / (allAppsIconSizePx + allAppsCellWidthGap); @@ -241,9 +258,9 @@ public class DeviceProfile { /** Returns the search bar top offset */ private int getSearchBarTopOffset() { if (isTablet && !isVerticalBarLayout()) { - return 4 * edgeMarginPx; + return searchBarVisible ? 4 * edgeMarginPx : 0; } else { - return 2 * edgeMarginPx; + return searchBarVisible ? 2 * edgeMarginPx : 0; } } @@ -269,12 +286,13 @@ public class DeviceProfile { (inv.numColumns * cellWidthPx)) / (2 * (inv.numColumns + 1))); bounds.set(edgeMarginPx + gap, getSearchBarTopOffset(), availableWidthPx - (edgeMarginPx + gap), - searchBarSpaceHeightPx); + searchBarVisible ? searchBarSpaceHeightPx : edgeMarginPx); } else { bounds.set(desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.left, getSearchBarTopOffset(), availableWidthPx - (desiredWorkspaceLeftRightMarginPx - - defaultWidgetPadding.right), searchBarSpaceHeightPx); + defaultWidgetPadding.right), searchBarVisible ? searchBarSpaceHeightPx : + edgeMarginPx); } } return bounds; @@ -378,15 +396,18 @@ public class DeviceProfile { return visibleChildren; } - public void layout(Launcher launcher) { - FrameLayout.LayoutParams lp; - boolean hasVerticalBarLayout = isVerticalBarLayout(); - final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources()); + public void layoutSearchBar(Launcher launcher, boolean hasVerticalBarLayout) { + // Update search bar for live settings + searchBarVisible = isSearchBarEnabled(launcher); // Layout the search bar space View searchBar = launcher.getSearchDropTargetBar(); - lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams(); + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams(); if (hasVerticalBarLayout) { + // If search bar is invisible add some extra padding for the drop targets + searchBarSpaceHeightPx = searchBarVisible ? searchBarSpaceHeightPx + : defaultSearchBarSpaceHeightPx + 5 * edgeMarginPx; + // Vertical search bar space -- The search bar is fixed in the layout to be on the left // of the screen regardless of RTL lp.gravity = Gravity.LEFT; @@ -394,7 +415,8 @@ public class DeviceProfile { LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar); targets.setOrientation(LinearLayout.VERTICAL); - FrameLayout.LayoutParams targetsLp = (FrameLayout.LayoutParams) targets.getLayoutParams(); + FrameLayout.LayoutParams targetsLp = + (FrameLayout.LayoutParams) targets.getLayoutParams(); targetsLp.gravity = Gravity.TOP; targetsLp.height = LayoutParams.WRAP_CONTENT; @@ -408,6 +430,23 @@ public class DeviceProfile { } searchBar.setLayoutParams(lp); + View qsbBar = launcher.getOrCreateQsbBar(); + if (qsbBar != null) { + qsbBar.setVisibility(searchBarVisible ? View.VISIBLE : View.GONE); + LayoutParams vglp = qsbBar.getLayoutParams(); + vglp.width = LayoutParams.MATCH_PARENT; + vglp.height = LayoutParams.MATCH_PARENT; + qsbBar.setLayoutParams(vglp); + } + } + + public void layout(Launcher launcher) { + FrameLayout.LayoutParams lp; + boolean hasVerticalBarLayout = isVerticalBarLayout(); + final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources()); + + layoutSearchBar(launcher, hasVerticalBarLayout); + // Layout the workspace PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace); lp = (FrameLayout.LayoutParams) workspace.getLayoutParams(); @@ -458,13 +497,13 @@ public class DeviceProfile { lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; lp.width = LayoutParams.WRAP_CONTENT; lp.height = LayoutParams.WRAP_CONTENT; - lp.bottomMargin = hotseatBarHeightPx; + lp.bottomMargin = Math.max(hotseatBarHeightPx , lp.bottomMargin); pageIndicator.setLayoutParams(lp); } } // Layout the Overview Mode - ViewGroup overviewMode = launcher.getOverviewPanel(); + /*ViewGroup overviewMode = launcher.getOverviewPanel(); if (overviewMode != null) { int overviewButtonBarHeight = getOverviewModeButtonBarHeight(); lp = (FrameLayout.LayoutParams) overviewMode.getLayoutParams(); @@ -501,7 +540,7 @@ public class DeviceProfile { } } } - } + }*/ } private int getCurrentWidth() { @@ -515,4 +554,24 @@ public class DeviceProfile { ? Math.min(widthPx, heightPx) : Math.max(widthPx, heightPx); } + + private boolean isSearchBarEnabled(Context context) { + boolean searchActivityExists = Utilities.searchActivityExists(context); + + boolean isSearchEnabled = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SEARCH, + R.bool.preferences_interface_homescreen_search_default); + + if (searchActivityExists) { + return isSearchEnabled; + } else { + if (isSearchEnabled) { + // Disable search bar + SettingsProvider.putBoolean(context, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SEARCH, false); + } + + return false; + } + } } diff --git a/src/com/android/launcher3/DeviceUnlockedReceiver.java b/src/com/android/launcher3/DeviceUnlockedReceiver.java new file mode 100644 index 000000000..6e800e338 --- /dev/null +++ b/src/com/android/launcher3/DeviceUnlockedReceiver.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 The CyanogenMod 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; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import java.util.HashSet; +import java.util.Set; + +public class DeviceUnlockedReceiver extends BroadcastReceiver { + public static final String INTENT_ACTION = Intent.ACTION_USER_PRESENT; + + private final Set<DeviceUnlockedListener> mListeners; + + interface DeviceUnlockedListener { + void onDeviceUnlocked(); + } + + public DeviceUnlockedReceiver() { + mListeners = new HashSet<DeviceUnlockedListener>(); + } + + public void registerListener(final DeviceUnlockedListener listener) { + mListeners.add(listener); + } + + public void deregisterListener(final DeviceUnlockedListener listener) { + mListeners.remove(listener); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!intent.getAction().equals(INTENT_ACTION)) return; + + for (DeviceUnlockedListener listener: mListeners) { + listener.onDeviceUnlocked(); + } + } +} diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java index 1c18747c1..03c5a1491 100644 --- a/src/com/android/launcher3/DragLayer.java +++ b/src/com/android/launcher3/DragLayer.java @@ -823,6 +823,7 @@ public class DragLayer extends InsettableFrameLayout { @Override public void onChildViewRemoved(View parent, View child) { + super.onChildViewRemoved(parent, child); updateChildIndices(); } diff --git a/src/com/android/launcher3/DynamicGridSizeFragment.java b/src/com/android/launcher3/DynamicGridSizeFragment.java new file mode 100644 index 000000000..103d660dd --- /dev/null +++ b/src/com/android/launcher3/DynamicGridSizeFragment.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.app.Dialog; +import android.app.Fragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.NumberPicker; +import android.widget.TextView; +import com.android.launcher3.settings.SettingsProvider; + +public class DynamicGridSizeFragment extends Fragment + implements NumberPicker.OnValueChangeListener, Dialog.OnDismissListener { + public static final String DYNAMIC_GRID_SIZE_FRAGMENT = "DynamicGridSizeFragment"; + + public static final int MIN_DYNAMIC_GRID_ROWS = 2; + public static final int MIN_DYNAMIC_GRID_COLUMNS = 3; + + GridSizeView mDynamicGrid; + + ListView mListView; + View mCurrentSelection; + GridSizeAdapter mAdapter; + InvariantDeviceProfile.GridSize mCurrentSize; + + Dialog mDialog; + + int mCustomGridRows = 0; + int mCustomGridColumns = 0; + + View.OnClickListener mSettingsItemListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + mCurrentSize = InvariantDeviceProfile.GridSize.getModeForValue((Integer) v.getTag()); + + setCleared(mCurrentSelection); + setSelected(v); + mCurrentSelection = v; + + if (mCurrentSize == InvariantDeviceProfile.GridSize.Custom) { + showNumberPicker(); + } + + ((GridSizeAdapter) mListView.getAdapter()).notifyDataSetChanged(); + + mAdapter.notifyDataSetInvalidated(); + updateGridMetrics(); + } + }; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.dynamic_grid_size_screen, container, false); + mDynamicGrid = (GridSizeView) v.findViewById(R.id.dynamic_grid_size_image); + mListView = (ListView) v.findViewById(R.id.dynamic_grid_list); + + Launcher launcher = (Launcher) getActivity(); + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) + mListView.getLayoutParams(); + lp.bottomMargin = ((FrameLayout.LayoutParams) launcher.getOverviewPanel() + .findViewById(R.id.settings_container).getLayoutParams()).bottomMargin; + mListView.setLayoutParams(lp); + + LinearLayout titleLayout = (LinearLayout) v.findViewById(R.id.dynamic_grid_title); + titleLayout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setSize(); + } + }); + + mCurrentSize = InvariantDeviceProfile.GridSize.getModeForValue( + SettingsProvider.getIntCustomDefault(getActivity(), + SettingsProvider.SETTINGS_UI_DYNAMIC_GRID_SIZE, 0)); + + InvariantDeviceProfile grid = getInvariantDeviceProfile(); + mCustomGridRows = grid.numRows; + mCustomGridColumns = grid.numColumns; + + updateGridMetrics(); + + Resources res = getResources(); + int[] valueResIds = { + R.string.grid_size_comfortable, + R.string.grid_size_cozy, + R.string.grid_size_condensed, + R.string.grid_size_custom + }; + mAdapter = new GridSizeAdapter(getActivity(), valueResIds); + mListView.setAdapter(mAdapter); + + // RTL + ImageView navPrev = (ImageView) v.findViewById(R.id.nav_prev); + Configuration config = getResources().getConfiguration(); + if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + navPrev.setImageResource(R.drawable.ic_navigation_next); + } + + return v; + } + + private void updateGridMetrics() { + if (mCurrentSize == InvariantDeviceProfile.GridSize.Custom) { + mDynamicGrid.setMetrics(mCustomGridRows, mCustomGridColumns); + } else { + InvariantDeviceProfile grid = getInvariantDeviceProfile(); + mDynamicGrid.setMetrics(grid.numRowsBase + mCurrentSize.getValue(), + grid.numColumnsBase + mCurrentSize.getValue()); + } + } + + @Override + public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { + if (enter) { + DisplayMetrics displaymetrics = new DisplayMetrics(); + getActivity().getWindowManager().getDefaultDisplay().getMetrics(displaymetrics); + int width = displaymetrics.widthPixels; + Configuration config = getResources().getConfiguration(); + final ObjectAnimator anim; + if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + anim = ObjectAnimator.ofFloat(this, "translationX", -width, 0); + } else { + anim = ObjectAnimator.ofFloat(this, "translationX", width, 0); + } + + final View darkPanel = ((Launcher) getActivity()).getDarkPanel(); + darkPanel.setVisibility(View.VISIBLE); + ObjectAnimator anim2 = ObjectAnimator.ofFloat(darkPanel, "alpha", 0.0f, 0.3f); + anim2.start(); + + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd (Animator animation) { + darkPanel.setVisibility(View.GONE); + } + }); + + return anim; + } else { + return super.onCreateAnimator(transit, enter, nextAnim); + } + } + + public void setSize() { + ((Launcher) getActivity()).setDynamicGridSize(mCurrentSize); + } + + private void setSelected(View v) { + v.setBackgroundColor(Color.WHITE); + TextView t = (TextView) v.findViewById(R.id.item_name); + t.setTextColor(getResources().getColor(R.color.settings_bg_color)); + } + + private void setCleared(View v) { + v.setBackgroundColor(getResources().getColor(R.color.settings_bg_color)); + TextView t = (TextView) v.findViewById(R.id.item_name); + t.setTextColor(Color.WHITE); + } + + private void showNumberPicker() { + mDialog = new Dialog(getActivity()); + mDialog.setTitle(getResources().getString( + R.string.preferences_interface_homescreen_custom)); + mDialog.setContentView(R.layout.custom_grid_size_dialog); + + NumberPicker nPRows = (NumberPicker) mDialog.findViewById(R.id.custom_rows); + NumberPicker nPColumns = (NumberPicker) mDialog.findViewById(R.id.custom_columns); + + InvariantDeviceProfile grid = getInvariantDeviceProfile(); + int rows = grid.numRowsBase; + int columns = grid.numColumnsBase; + + nPRows.setMinValue(Math.max(MIN_DYNAMIC_GRID_ROWS, rows - InvariantDeviceProfile.GRID_SIZE_MIN)); + nPRows.setMaxValue(rows + InvariantDeviceProfile.GRID_SIZE_MAX); + nPRows.setValue(mCustomGridRows); + nPRows.setWrapSelectorWheel(false); + nPRows.setOnValueChangedListener(this); + nPRows.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + + nPColumns.setMinValue(Math.max(MIN_DYNAMIC_GRID_COLUMNS, + columns - InvariantDeviceProfile.GRID_SIZE_MIN)); + nPColumns.setMaxValue(columns + InvariantDeviceProfile.GRID_SIZE_MAX); + nPColumns.setValue(mCustomGridColumns); + nPColumns.setWrapSelectorWheel(false); + nPColumns.setOnValueChangedListener(this); + nPColumns.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + + Button button = (Button) mDialog.findViewById(R.id.dialog_confirm_button); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mDialog != null) { + mDialog.dismiss(); + } + } + }); + + mDialog.setOnDismissListener(this); + mDialog.show(); + } + + @Override + public void onPause() { + super.onPause(); + if (mDialog != null) { + mDialog.dismiss(); + } + } + + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + if (picker.getId() == R.id.custom_rows) { + mCustomGridRows = newVal; + } else if (picker.getId() == R.id.custom_columns) { + mCustomGridColumns = newVal; + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + SettingsProvider.putInt(getActivity(), + SettingsProvider.SETTINGS_UI_HOMESCREEN_ROWS, mCustomGridRows); + SettingsProvider.putInt(getActivity(), + SettingsProvider.SETTINGS_UI_HOMESCREEN_COLUMNS, mCustomGridColumns); + + mAdapter.notifyDataSetInvalidated(); + mDynamicGrid.setMetrics(mCustomGridRows, mCustomGridColumns); + } + + private class GridSizeAdapter extends BaseAdapter { + Context mContext; + int[] mTitleResIds; + + public GridSizeAdapter(Context context, int[] resIds) { + mContext = context; + mTitleResIds = resIds; + } + + @Override + public int getCount() { + return mTitleResIds.length; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public Object getItem(int position) { + return mContext.getString(mTitleResIds[position]); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + LayoutInflater inflater = (LayoutInflater) + mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.settings_pane_list_item, parent, false); + } + + TextView textView = (TextView) convertView.findViewById(R.id.item_name); + + // RTL + Configuration config = getResources().getConfiguration(); + if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + textView.setGravity(Gravity.RIGHT); + } + + // Set selected state + if (position == mCurrentSize.getValue()) { + if (mCurrentSelection != null) { + setCleared(mCurrentSelection); + } + mCurrentSelection = convertView; + setSelected(mCurrentSelection); + } + + if (position == InvariantDeviceProfile.GridSize.Custom.getValue()) { + InvariantDeviceProfile grid = getInvariantDeviceProfile(); + + int rows = SettingsProvider.getIntCustomDefault(getActivity(), + SettingsProvider.SETTINGS_UI_HOMESCREEN_ROWS, grid.numRowsBase); + int columns = SettingsProvider.getIntCustomDefault(getActivity(), + SettingsProvider.SETTINGS_UI_HOMESCREEN_COLUMNS, grid.numColumnsBase); + textView.setText(mContext.getString(mTitleResIds[position], rows, columns)); + } else { + textView.setText(mTitleResIds[position]); + } + + convertView.setOnClickListener(mSettingsItemListener); + convertView.setTag(position); + return convertView; + } + } + + private InvariantDeviceProfile getInvariantDeviceProfile() { + LauncherAppState app = LauncherAppState.getInstance(); + return app.getInvariantDeviceProfile(); + } + + private static class GridSizeView extends View { + private int mRows = 0, mColumns = 0; + private Paint mForegroundPaint; + private int mBackgroundColor; + + public GridSizeView(Context context, AttributeSet attrs) { + super(context, attrs); + Resources res = context.getResources(); + + mForegroundPaint = new Paint(); + mForegroundPaint.setColor(res.getColor(R.color.dynamic_grid_preview_foreground)); + mBackgroundColor = res.getColor(R.color.dynamic_grid_preview_background); + } + + public void setMetrics(int rows, int columns) { + mRows = rows; + mColumns = columns; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + float width = getWidth() - getPaddingLeft() - getPaddingRight(); + float height = getHeight() - getPaddingTop() - getPaddingBottom(); + float xOffset = getPaddingLeft(); + float yOffset = getPaddingTop(); + + canvas.drawColor(mBackgroundColor); + + // Draw rows + for (int i = 1; i < mRows; i++) { + float yPos = yOffset + height / mRows * i; + canvas.drawLine(xOffset, yPos, xOffset + width, yPos, mForegroundPaint); + } + + // Draw columns + for (int j = 1; j < mColumns; j++) { + float xPos = xOffset + width / mColumns * j; + canvas.drawLine(xPos, yOffset, xPos, yOffset + height, mForegroundPaint); + } + } + } +} diff --git a/src/com/android/launcher3/FocusIndicatorView.java b/src/com/android/launcher3/FocusIndicatorView.java index ecf93e4b3..c30b56177 100644 --- a/src/com/android/launcher3/FocusIndicatorView.java +++ b/src/com/android/launcher3/FocusIndicatorView.java @@ -165,6 +165,9 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen private static void computeLocationRelativeToParentHelper(View child, View commonParent, int[] shift) { + if (child == null) { + return; + } View parent = (View) child.getParent(); shift[0] += child.getLeft(); shift[1] += child.getTop(); diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index c1aa35669..1ac5cf85e 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -23,18 +23,26 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; +import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.PowerManager; +import android.provider.Settings; import android.text.InputType; import android.text.Selection; import android.text.Spannable; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.view.ActionMode; import android.view.KeyEvent; import android.view.Menu; @@ -46,8 +54,11 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.AccelerateInterpolator; import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; +import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -57,12 +68,18 @@ import com.android.launcher3.FolderInfo.FolderListener; import com.android.launcher3.UninstallDropTarget.UninstallSource; import com.android.launcher3.Workspace.ItemOperator; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.UiThreadCircularReveal; +import static cyanogenmod.content.Intent.ACTION_PROTECTED; +import static cyanogenmod.content.Intent.EXTRA_PROTECTED_COMPONENTS; +import static cyanogenmod.content.Intent.EXTRA_PROTECTED_STATE; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.List; /** * Represents a set of icons chosen by the user or generated by the system. @@ -89,6 +106,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList */ public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY; + private static final int CLOSE_FOLDER_DELAY_MS = 150; + + private static final int ALPHA_DELAY_MULT = 15; + /** * Fraction of icon width which behave as scroll region. */ @@ -96,6 +117,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList private static final int FOLDER_NAME_ANIMATION_DURATION = 633; + private static final int REORDER_ANIMATION_DURATION = 230; private static final int REORDER_DELAY = 250; private static final int ON_EXIT_CLOSE_DELAY = 400; private static final Rect sTempRect = new Rect(); @@ -116,6 +138,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList private final InputMethodManager mInputMethodManager; + private final PowerManager mPowerManager; + protected final Launcher mLauncher; protected DragController mDragController; protected FolderInfo mInfo; @@ -127,7 +151,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList ExtendedEditText mFolderName; private View mFooter; - private int mFooterHeight; // Cell ranks used for drag and drop @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank; @@ -147,6 +170,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Thunk float mFolderIconPivotY; private boolean mIsEditingName = false; + ImageView mFolderLock; + private boolean mDestroyed; @Thunk Runnable mDeferredAction; @@ -156,6 +181,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Folder scrolling private int mScrollAreaOffset; + private Handler mHandler; + @Thunk int mScrollHintDir = DragController.SCROLL_NONE; @Thunk int mCurrentScrollDir = DragController.SCROLL_NONE; @@ -168,9 +195,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public Folder(Context context, AttributeSet attrs) { super(context, attrs); setAlwaysDrawnWithCacheEnabled(false); + mHandler = new Handler(); mInputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + Resources res = getResources(); mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration); mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration); @@ -214,13 +244,16 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mFolderName.setInputType(mFolderName.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS); - mFooter = findViewById(R.id.folder_footer); + boolean hideLabels = SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + if (hideLabels) { + mFolderName.setVisibility(View.GONE); + } + mFolderLock = (ImageView) findViewById(R.id.folder_lock); + mFolderLock.setOnClickListener(this); - // We find out how tall footer wants to be (it is set to wrap_content), so that - // we can allocate the appropriate amount of space for it. - int measureSpec = MeasureSpec.UNSPECIFIED; - mFooter.measure(measureSpec, measureSpec); - mFooterHeight = mFooter.getMeasuredHeight(); + mFooter = findViewById(R.id.folder_footer); } private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { @@ -245,6 +278,72 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList if (tag instanceof ShortcutInfo) { mLauncher.onClick(v); } + + if (v.getId() == R.id.folder_lock) { + startHiddenFolderManager(mInfo.hidden ? + Launcher.REQUEST_UNPROTECT_FOLDER : + Launcher.REQUEST_PROTECT_FOLDER); + } + } + + public void startHiddenFolderManager(int action) { + mLauncher.validateLockForHiddenFolders(mFolderIcon, action); + } + + public List<Pair<ComponentName, CharSequence>> getComponents() { + int size = mItemsInReadingOrder.size(); + List<Pair<ComponentName, CharSequence>> components = + new ArrayList<Pair<ComponentName, CharSequence>>(); + + for (int i = 0; i < size; i++) { + View v = mItemsInReadingOrder.get(i); + Object tag = v.getTag(); + if (tag instanceof ShortcutInfo) { + ShortcutInfo shortcut = (ShortcutInfo) tag; + components.add(Pair.create(shortcut.getIntent().getComponent(), shortcut.title)); + } + } + + return components; + } + + public void modifyProtectedApps(boolean protect) { + ArrayList<ComponentName> components = new ArrayList<>(); + for (Pair<ComponentName, CharSequence> item : getComponents()) { + if (item.first != null) { + components.add(item.first); + } + } + + Intent intent = new Intent(); + intent.setAction(ACTION_PROTECTED); + // flip the boolean value to accomodate framework + // in framework "false" is "protected" and "true" is "visible" + intent.putExtra(EXTRA_PROTECTED_STATE, !protect); + intent.putExtra(EXTRA_PROTECTED_COMPONENTS, components); + + mLauncher.sendBroadcast(intent); + } + + private void removeProtectedApp(ComponentName componentName) { + ArrayList<ComponentName> components = new ArrayList<>(); + components.add(componentName); + Intent intent = new Intent(); + intent.setAction(ACTION_PROTECTED); + // flip the boolean value to accomodate framework + // in framework "false" is "protected" and "true" is "visible" + intent.putExtra(EXTRA_PROTECTED_STATE, true); + intent.putExtra(EXTRA_PROTECTED_COMPONENTS, components); + + mLauncher.sendBroadcast(intent); + } + + public void saveHiddenFolderState(boolean protect) { + mInfo.hidden = protect; + modifyProtectedApps(protect); + LauncherModel.updateItemInDatabase(mLauncher, mInfo); + mLauncher.mModel.flushWorkerThread(); + rebind(mInfo); } public boolean onLongClick(View v) { @@ -311,7 +410,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Convert to a string here to ensure that no other state associated with the text field // gets saved. String newTitle = mFolderName.getText().toString(); - mInfo.setTitle(newTitle); + if (!SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default)) { + mInfo.setTitle(newTitle); + } LauncherModel.updateItemInDatabase(mLauncher, mInfo); if (commit) { @@ -324,6 +427,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList Selection.setSelection((Spannable) mFolderName.getText(), 0, 0); mIsEditingName = false; + mLauncher.notifyFolderNameChanged(); } public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { @@ -368,7 +472,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mInfo; } - void bind(FolderInfo info) { + void bind(final FolderInfo info) { mInfo = info; ArrayList<ShortcutInfo> children = info.contents; Collections.sort(children, ITEM_POS_COMPARATOR); @@ -383,18 +487,19 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList LauncherModel.deleteItemFromDatabase(mLauncher, item); } - DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); - if (lp == null) { - lp = new DragLayer.LayoutParams(0, 0); - lp.customPosition = true; - setLayoutParams(lp); - } - centerAboutIcon(); - mItemsInvalidated = true; updateTextViewFocus(); mInfo.addListener(this); + setFolderName(); + updateLock(); + } + + public void rebind(final FolderInfo info) { + bind(info); + } + + public void setFolderName() { if (!sDefaultFolderName.contentEquals(mInfo.title)) { mFolderName.setText(mInfo.title); } else { @@ -411,16 +516,25 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList }); } + private void updateLock() { + if (mInfo != null) { + mFolderLock.setImageResource(mInfo.hidden ? + R.drawable.folder_locked : + R.drawable.folder_unlocked); + } + } + /** * Creates a new UserFolder, inflated from R.layout.user_folder. * * @param context The application's context. + * @param root The {@link View} parent of this folder. * * @return A new UserFolder. */ @SuppressLint("InflateParams") - static Folder fromXml(Launcher launcher) { - return (Folder) launcher.getLayoutInflater().inflate(R.layout.user_folder, null); + static Folder fromXml(Launcher launcher, ViewGroup root) { + return (Folder) launcher.getLayoutInflater().inflate(R.layout.user_folder, root, false); } /** @@ -440,6 +554,39 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList setScaleY(1f); setAlpha(1f); mState = STATE_SMALL; + + View reveal = mLauncher.findViewById(R.id.reveal_fake_page_container); + reveal.setVisibility(View.VISIBLE); + View revealFolderIcon = mLauncher.findViewById(R.id.reveal_fake_folder_icon); + revealFolderIcon.setVisibility(View.INVISIBLE); + } + + private void prepareFakeFolderIcon() { + mFolderIcon.destroyDrawingCache(); + mFolderIcon.buildDrawingCache(true); + + Bitmap fakeFolderIcon = Bitmap.createBitmap(mFolderIcon.getDrawingCache()); + View fakeFolderIconView = mLauncher.findViewById(R.id.reveal_fake_folder_icon); + FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) + fakeFolderIconView.getLayoutParams(); + + // Get globalVisibleRect of the folderIcon. getWidth and getHeight are inaccurate for + // hotseat icons + Rect rect = new Rect(); + mFolderIcon.getGlobalVisibleRect(rect); + + flp.height = rect.height(); + flp.width = rect.width(); + + fakeFolderIconView.setLayoutParams(flp); + + int [] folderIconXY = new int[2]; + mFolderIcon.getLocationOnScreen(folderIconXY); + fakeFolderIconView.setX(folderIconXY[0]); + fakeFolderIconView.setY(folderIconXY[1]); + + fakeFolderIconView.setBackground(new BitmapDrawable(null, fakeFolderIcon)); + fakeFolderIconView.setVisibility(View.INVISIBLE); } public void animateOpen() { @@ -455,7 +602,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList final Runnable onCompleteRunnable; if (!Utilities.ATLEAST_LOLLIPOP) { positionAndSizeAsIcon(); - centerAboutIcon(); + calculatePivot(); PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1); PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); @@ -473,12 +620,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } }; } else { - prepareReveal(); - centerAboutIcon(); + calculatePivot(); AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); - int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); - int height = getFolderHeight(); + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); float transX = - 0.075f * (width / 2 - getPivotX()); float transY = - 0.075f * (height / 2 - getPivotY()); @@ -490,7 +636,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList Animator drift = ObjectAnimator.ofPropertyValuesHolder(this, tx, ty); drift.setDuration(mMaterialExpandDuration); drift.setStartDelay(mMaterialExpandStagger); - drift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + drift.setInterpolator(new LogDecelerateInterpolator(60, 0)); int rx = (int) Math.max(Math.max(width - getPivotX(), 0), getPivotX()); int ry = (int) Math.max(Math.max(height - getPivotY(), 0), getPivotY()); @@ -513,6 +659,34 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList textAlpha.setStartDelay(mMaterialExpandStagger); textAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); + prepareFakeFolderIcon(); + float iconTransY = getResources().getInteger(R.integer.folder_icon_translate_y_dist); + + final View fakeFolderIconView = mLauncher.findViewById(R.id.reveal_fake_folder_icon); + float baseIconTranslationY = fakeFolderIconView.getTranslationY(); + PropertyValuesHolder iconty = PropertyValuesHolder.ofFloat("translationY", + baseIconTranslationY, baseIconTranslationY + iconTransY); + PropertyValuesHolder iconAlpha = PropertyValuesHolder.ofFloat("alpha", 1f, 0f); + + Animator fakeFolderIcon = LauncherAnimUtils.ofPropertyValuesHolder(fakeFolderIconView, + iconty, iconAlpha); + fakeFolderIcon.setDuration(mMaterialExpandDuration); + fakeFolderIcon.setInterpolator(new AccelerateInterpolator(1.5f)); + fakeFolderIcon.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mFolderIcon.setAlpha(0); + fakeFolderIconView.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animation) { + fakeFolderIconView.setVisibility(View.INVISIBLE); + } + }); + + prepareReveal(); + anim.play(drift); anim.play(iconsAlpha); anim.play(textAlpha); @@ -526,7 +700,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void run() { mContentWrapper.setLayerType(LAYER_TYPE_NONE, null); - mContentWrapper.setLayerType(LAYER_TYPE_NONE, null); } }; } @@ -535,6 +708,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void onAnimationStart(Animator animation) { sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, mContent.getAccessibilityDescription()); + hideWorkspace(); mState = STATE_ANIMATING; } @Override @@ -628,31 +802,128 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } - public void animateClosed() { + public int getState() { + return mState; + } + + public void animateClosed(final boolean animate) { if (!(getParent() instanceof DragLayer)) return; - PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); - PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f); - PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f); - final ObjectAnimator oa = - LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); + AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); - oa.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - onCloseComplete(); - setLayerType(LAYER_TYPE_NONE, null); - mState = STATE_SMALL; + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f); + float transY = getResources().getInteger(R.integer.folder_translate_y_dist); + PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", 0f, + transY); + + setLayerType(LAYER_TYPE_HARDWARE, null); + + float animatorDurationScale = Settings.Global.getFloat(getContext().getContentResolver(), + Settings.Global.ANIMATOR_DURATION_SCALE, 1); + ObjectAnimator oa; + if (mPowerManager.isPowerSaveMode() || animatorDurationScale < 0.01f) { + // power save mode is no fun - skip alpha animation and just set it to 0 + // otherwise the icons will stay around until the duration of the animation + if (animate) { + oa = LauncherAnimUtils.ofPropertyValuesHolder(this, translationY); + } else { + oa = LauncherAnimUtils.ofPropertyValuesHolder(this); + } + setAlpha(0f); + } else { + if (animate) { + oa = LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, translationY); + } else { + oa = LauncherAnimUtils.ofPropertyValuesHolder(this, alpha); } + } + + oa.setDuration(mMaterialExpandDuration); + oa.setInterpolator(new LogDecelerateInterpolator(60, 0)); + anim.play(oa); + + Animator reverseRevealAnim = null; + Animator fakeFolderIconAnim = null; + + if (animate) { + + prepareFakeFolderIcon(); + float iconTransY = getResources().getInteger(R.integer.folder_icon_translate_y_dist); + + final View fakeFolderIconView = mLauncher.findViewById(R.id.reveal_fake_folder_icon); + float baseIconTranslationY = fakeFolderIconView.getTranslationY(); + PropertyValuesHolder iconty = PropertyValuesHolder.ofFloat("translationY", + baseIconTranslationY + iconTransY, baseIconTranslationY); + PropertyValuesHolder iconAlpha = PropertyValuesHolder.ofFloat("alpha", 0f, 1f); + + fakeFolderIconAnim = LauncherAnimUtils.ofPropertyValuesHolder(fakeFolderIconView, + iconty, iconAlpha); + fakeFolderIconAnim.setDuration(mMaterialExpandDuration); + fakeFolderIconAnim.setInterpolator(new DecelerateInterpolator(2f)); + fakeFolderIconAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mFolderIcon.setAlpha(0); + fakeFolderIconView.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animation) { + fakeFolderIconView.setVisibility(View.INVISIBLE); + mFolderIcon.setAlpha(1); + + View revealView = mLauncher.findViewById(R.id.reveal_fake_page_container); + revealView.setVisibility(View.INVISIBLE); + } + }); + } else { + View revealView = mLauncher.findViewById(R.id.reveal_fake_page_container); + revealView.setVisibility(View.INVISIBLE); + mFolderIcon.setAlpha(1); + } + + if (reverseRevealAnim != null) { + anim.play(reverseRevealAnim); + } + if (fakeFolderIconAnim != null) { + anim.play(fakeFolderIconAnim); + } + + anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, getContext().getString(R.string.folder_closed)); + unHideWorkspace(); mState = STATE_ANIMATING; } + + @Override + public void onAnimationEnd(Animator animation) { + onCloseComplete(); + setLayerType(LAYER_TYPE_NONE, null); + mState = STATE_SMALL; + } }); - oa.setDuration(mExpandDuration); - setLayerType(LAYER_TYPE_HARDWARE, null); - oa.start(); + + anim.start(); + } + + private int mSavedWidgetsVisibilityState = INVISIBLE; + private void hideWorkspace() { + mSavedWidgetsVisibilityState = mLauncher.getWidgetsView().getVisibility(); + mLauncher.getWidgetsView().setVisibility(INVISIBLE); + mLauncher.getWorkspace().setVisibility(INVISIBLE); + mLauncher.getHotseat().setVisibility(INVISIBLE); + mLauncher.getSearchDropTargetBar().setVisibility(INVISIBLE); + mLauncher.getPageIndicator().setVisibility(INVISIBLE); + } + + private void unHideWorkspace() { + mLauncher.getWidgetsView().setVisibility(mSavedWidgetsVisibilityState); + mLauncher.getWorkspace().setVisibility(VISIBLE); + mLauncher.getHotseat().setVisibility(VISIBLE); + mLauncher.getSearchDropTargetBar().setVisibility(VISIBLE); + mLauncher.getPageIndicator().setVisibility(VISIBLE); } public boolean acceptDrop(DragObject d) { @@ -847,6 +1118,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } mScrollPauseAlarm.cancelAlarm(); completeDragExit(); + if (successfulDrop) { + ShortcutInfo info = (ShortcutInfo) d.dragInfo; + Intent intent = info.getIntent(); + if (intent != null && mInfo.hidden) { + removeProtectedApp(intent.getComponent()); + } + } } } @@ -914,7 +1192,16 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Do nothing } + /** + * @return true if contents should persist their status to the database. + */ + protected boolean shouldUpdateContentsInDatabase() { + return true; + } + private void updateItemLocationsInDatabaseBatch() { + if (!shouldUpdateContentsInDatabase()) return; + ArrayList<View> list = getItemsInReadingOrder(); ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); for (int i = 0; i < list.size(); i++) { @@ -928,6 +1215,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } public void addItemLocationsInDatabase() { + if (!shouldUpdateContentsInDatabase()) return; + ArrayList<View> list = getItemsInReadingOrder(); for (int i = 0; i < list.size(); i++) { View v = list.get(i); @@ -951,12 +1240,22 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mContent.isFull(); } - private void centerAboutIcon() { - DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + protected void calculatePivot() { + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + + // If we haven't measured ourselves yet, force one now to determine our dimensions. + if (width <= 0 && height <= 0) { + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + width = getMeasuredWidth(); + height = getMeasuredHeight(); + } + + calculatePivot(width, height); + } + private void calculatePivot(int width, int height) { DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer); - int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); - int height = getFolderHeight(); float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect); @@ -966,22 +1265,22 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList int centerY = (int) (sTempRect.top + sTempRect.height() * scale / 2); int centeredLeft = centerX - width / 2; int centeredTop = centerY - height / 2; + int currentPage = mLauncher.getWorkspace().getNextPage(); + + // We first fetch the currently visible CellLayoutChildren + CellLayout currentLayout = (CellLayout) mLauncher.getWorkspace().getChildAt(currentPage); + ShortcutAndWidgetContainer boundingLayout = currentLayout.getShortcutsAndWidgets(); + Rect bounds = new Rect(); + parent.getDescendantRectRelativeToSelf(boundingLayout, bounds); - // We need to bound the folder to the currently visible workspace area - mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect); - int left = Math.min(Math.max(sTempRect.left, centeredLeft), - sTempRect.left + sTempRect.width() - width); - int top = Math.min(Math.max(sTempRect.top, centeredTop), - sTempRect.top + sTempRect.height() - height); - if (grid.isPhone && (grid.availableWidthPx - width) < grid.iconSizePx) { - // Center the folder if it is full (on phones only) - left = (grid.availableWidthPx - width) / 2; - } else if (width >= sTempRect.width()) { + // Center the folder + int left = (grid.availableWidthPx - width) / 2; + // Drop the top down a little so it isn't bounded by the page indicators + int top = (int) (bounds.top + (bounds.height() * 1.15) - height); + + if (width >= bounds.width()) { // If the folder doesn't fit within the bounds, center it about the desired bounds - left = sTempRect.left + (sTempRect.width() - width) / 2; - } - if (height >= sTempRect.height()) { - top = sTempRect.top + (sTempRect.height() - height) / 2; + left = bounds.left + (bounds.width() - width) / 2; } int folderPivotX = width / 2 + (centeredLeft - left); @@ -992,11 +1291,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList (1.0f * folderPivotX / width)); mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() * (1.0f * folderPivotY / height)); - - lp.width = width; - lp.height = height; - lp.x = left; - lp.y = top; } float getPivotXForIconAnimation() { @@ -1006,53 +1300,18 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mFolderIconPivotY; } - private int getContentAreaHeight() { - DeviceProfile grid = mLauncher.getDeviceProfile(); - Rect workspacePadding = grid.getWorkspacePadding(mContent.mIsRtl); - int maxContentAreaHeight = grid.availableHeightPx - - workspacePadding.top - workspacePadding.bottom - - mFooterHeight; - int height = Math.min(maxContentAreaHeight, - mContent.getDesiredHeight()); - return Math.max(height, MIN_CONTENT_DIMEN); + protected int getContentAreaHeight() { + return Math.max(mContent.getDesiredHeight(), MIN_CONTENT_DIMEN); } - private int getContentAreaWidth() { + protected int getContentAreaWidth() { return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN); } - private int getFolderHeight() { - return getFolderHeight(getContentAreaHeight()); - } - - private int getFolderHeight(int contentAreaHeight) { - return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight; - } - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int contentWidth = getContentAreaWidth(); - int contentHeight = getContentAreaHeight(); - - int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY); - int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY); + mContent.setFixedSize(getContentAreaWidth(), getContentAreaHeight()); - mContent.setFixedSize(contentWidth, contentHeight); - mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec); - - if (mContent.getChildCount() > 0) { - int cellIconGap = (mContent.getPageAt(0).getCellWidth() - - mLauncher.getDeviceProfile().iconSizePx) / 2; - mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap, - mFooter.getPaddingTop(), - mContent.getPaddingRight() + cellIconGap, - mFooter.getPaddingBottom()); - } - mFooter.measure(contentAreaWidthSpec, - MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY)); - - int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth; - int folderHeight = getFolderHeight(contentHeight); - setMeasuredDimension(folderWidth, folderHeight); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); } /** @@ -1115,7 +1374,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList View child = null; // Move the item from the folder to the workspace, in the position of the folder - if (getItemCount() == 1) { + if (!mInfo.hidden && getItemCount() == 1) { ShortcutInfo finalItem = mInfo.contents.get(0); child = mLauncher.createShortcut(cellLayout, finalItem); LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, @@ -1203,14 +1462,15 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank); // Actually move the item in the database if it was an external drag. Call this // before creating the view, so that ShortcutInfo is updated appropriately. - LauncherModel.addOrMoveItemInDatabase( - mLauncher, si, mInfo.id, 0, si.cellX, si.cellY); + if (shouldUpdateContentsInDatabase()) { + LauncherModel.addOrMoveItemInDatabase( + mLauncher, si, mInfo.id, 0, si.cellX, si.cellY); + } // We only need to update the locations if it doesn't get handled in #onDropCompleted. if (d.dragSource != this) { updateItemLocationsInDatabaseBatch(); } - mIsExternalDrag = false; } else { currentDragView = mCurrentDragView; mContent.addViewForRank(currentDragView, si, mEmptyCellRank); @@ -1246,6 +1506,17 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // The animation has already been shown while opening the folder. mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher); } + + if (mIsExternalDrag) { + // Delay the close animation to reduce jank + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + mLauncher.closeFolder(Folder.this); + } + }, 100); + mIsExternalDrag = false; + } } // This is used so the item doesn't immediately appear in the folder when added. In one case @@ -1267,8 +1538,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList if (mSuppressOnAdd) return; mContent.createAndAddViewForRank(item, mContent.allocateRankForNewItem(item)); mItemsInvalidated = true; - LauncherModel.addOrMoveItemInDatabase( - mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); + + if (shouldUpdateContentsInDatabase()) { + LauncherModel.addOrMoveItemInDatabase( + mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); + } } public void onRemove(ShortcutInfo item) { @@ -1276,8 +1550,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // If this item is being dragged from this open folder, we have already handled // the work associated with removing the item, so we don't have to do anything here. if (item == mCurrentDragInfo) return; - View v = getViewForInfo(item); - mContent.removeItem(v); + mContent.removeItem(getViewForInfo(item)); if (mState == STATE_ANIMATING) { mRearrangeOnClose = true; } else { @@ -1288,14 +1561,68 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } - private View getViewForInfo(final ShortcutInfo item) { - return mContent.iterateOverItems(new ItemOperator() { + @Override + public void onRemoveAll() { + // Clear the UX after folder contents are removed from the DB + removeViewsForItems(null); + mLauncher.closeFolder(this); + replaceFolderWithFinalItem(); + } - @Override - public boolean evaluate(ItemInfo info, View view, View parent) { - return info == item; + @Override + public void onRemoveAll(ArrayList<ShortcutInfo> items) { + removeViewsForItems(items); + if (mState == STATE_ANIMATING) { + mRearrangeOnClose = true; + } else { + rearrangeChildren(); + } + if (mInfo.contents.isEmpty()) { + mLauncher.closeFolder(this); + } + replaceFolderWithFinalItem(); + } + + /** + * Remove all the supplied item views from this folder. + * @param items info of views to remove, or null if all views should be removed. + */ + protected void removeViewsForItems(ArrayList<ShortcutInfo> items) { + mItemsInvalidated = true; + if (items == null) { + mContent.removeAllItems(); + } else { + for (ShortcutInfo item : items) { + mContent.removeItem(getViewForInfo(item)); } - }); + } + } + + /** + * Update the view tied to this shortcut. + * @param info updated info to be applied to view. + * @return true if view for info was found, false otherwise. + */ + @SuppressWarnings("unused") + public boolean updateViewForInfo(final ShortcutInfo info) { + View v = getViewForInfo(info); + if (v != null & v instanceof BubbleTextView) { + ((BubbleTextView) v).reapplyItemInfo(info); + + mItemsInvalidated = true; + return true; + } + + return false; + } + + public View getViewForInfo(ShortcutInfo item) { + View v = mContent.getChildAtRank(item.rank); + if (v != null && v.getTag() == item) { + return v; + } + + return null; } public void onItemsChanged() { @@ -1338,6 +1665,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList outRect.right += mScrollAreaOffset; } + public View getViewFromPosition(int position) { + return mItemsInReadingOrder.get(position); + } + @Override public void fillInLaunchSourceData(Bundle sourceData) { // Fill in from the folder icon's launch source provider first diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java index 8d534d2fe..356c2754b 100644 --- a/src/com/android/launcher3/FolderIcon.java +++ b/src/com/android/launcher3/FolderIcon.java @@ -39,6 +39,7 @@ import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.RelativeLayout; import android.widget.TextView; import com.android.launcher3.DropTarget.DragObject; @@ -60,17 +61,17 @@ public class FolderIcon extends FrameLayout implements FolderListener { private StylusEventHelper mStylusEventHelper; // The number of icons to display in the - public static final int NUM_ITEMS_IN_PREVIEW = 3; + public static final int NUM_ITEMS_IN_PREVIEW = 4; private static final int CONSUMPTION_ANIMATION_DURATION = 100; private static final int DROP_IN_ANIMATION_DURATION = 400; private static final int INITIAL_ITEM_ANIMATION_DURATION = 350; private static final int FINAL_ITEM_ANIMATION_DURATION = 200; // The degree to which the inner ring grows when accepting drop - private static final float INNER_RING_GROWTH_FACTOR = 0.15f; + private static final float INNER_RING_GROWTH_FACTOR = 0.0f; // The degree to which the outer ring is scaled in its natural state - private static final float OUTER_RING_GROWTH_FACTOR = 0.3f; + private static final float OUTER_RING_GROWTH_FACTOR = 0.1f; // The amount of vertical spread between items in the stack [0...1] private static final float PERSPECTIVE_SHIFT_FACTOR = 0.18f; @@ -90,7 +91,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { public static Drawable sSharedFolderLeaveBehind = null; - @Thunk ImageView mPreviewBackground; + @Thunk View mPreviewBackground; @Thunk BubbleTextView mFolderName; FolderRingAnimator mFolderRingAnimator = null; @@ -161,11 +162,11 @@ public class FolderIcon extends FrameLayout implements FolderListener { lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx; // Offset the preview background to center this view accordingly - icon.mPreviewBackground = (ImageView) icon.findViewById(R.id.preview_background); + icon.mPreviewBackground = icon.findViewById(R.id.preview_background); lp = (FrameLayout.LayoutParams) icon.mPreviewBackground.getLayoutParams(); - lp.topMargin = grid.folderBackgroundOffset; - lp.width = grid.folderIconSizePx; - lp.height = grid.folderIconSizePx; + lp.width = grid.iconSizePx; + lp.height = grid.iconSizePx; + icon.mPreviewBackground.setLayoutParams(lp); icon.setTag(folderInfo); icon.setOnClickListener(launcher); @@ -173,7 +174,16 @@ public class FolderIcon extends FrameLayout implements FolderListener { icon.mLauncher = launcher; icon.setContentDescription(String.format(launcher.getString(R.string.folder_name_format), folderInfo.title)); - Folder folder = Folder.fromXml(launcher); + Folder folder; + if (folderInfo.isRemote()) { + folder = launcher.getRemoteFolderManager().createRemoteFolder(icon, launcher.getDragLayer()); + if (folder == null) { + LauncherModel.deleteItemFromDatabase(launcher, folderInfo); + return null; + } + } else { + folder = Folder.fromXml(launcher, launcher.getDragLayer()); + } folder.setDragController(launcher.getDragController()); folder.setFolderIcon(icon); folder.bind(folderInfo); @@ -183,6 +193,63 @@ public class FolderIcon extends FrameLayout implements FolderListener { folderInfo.addListener(icon); icon.setOnFocusChangeListener(launcher.mFocusHandler); + icon.setDrawingCacheEnabled(true); + + // get dimen for the icon size + // padding is equal to 1/8 of icon size + // padding gets used at start and end accounting for 2/8 + // small icons are separated by 1/2 padding + // Total padding equals 2.5/8 leaving 5.5/8 for icons + // 5.5/8 remaining, divided by 2 equals 2.75 for each small icon + int padding = grid.iconSizePx / 8; + int smallIconSize = (int) (padding * 2.75); + + for (int i = NUM_ITEMS_IN_PREVIEW; i >= 0; i--) { + ImageView appIcon = null; + int marginLeft = 0, marginRight = 0, marginTop = 0, marginBottom = 0; + switch(i) { + case 0: + appIcon = (ImageView) icon.findViewById(R.id.app_0); + marginLeft = padding; + marginTop = padding; + break; + case 1: + appIcon = (ImageView) icon.findViewById(R.id.app_1); + marginTop = padding; + marginRight = padding; + break; + case 2: + appIcon = (ImageView) icon.findViewById(R.id.app_2); + marginBottom = padding; + marginLeft = padding; + break; + case 3: + appIcon = (ImageView) icon.findViewById(R.id.app_3); + marginBottom = padding; + marginRight = padding; + break; + } + + if (appIcon != null) { + RelativeLayout.LayoutParams layoutParams + = (RelativeLayout.LayoutParams) appIcon.getLayoutParams(); + + layoutParams.width = smallIconSize; + layoutParams.height = smallIconSize; + layoutParams.leftMargin = marginLeft; + layoutParams.rightMargin = marginRight; + layoutParams.topMargin = marginTop; + layoutParams.bottomMargin = marginBottom; + + appIcon.setLayoutParams(layoutParams); + } + } + + // Create an overlay badge if this FolderIcon is for a RemoteFolder + if (folderInfo.isRemote()) { + icon = RemoteFolderManager.addBadgeToFolderIcon(icon); + } + return icon; } @@ -220,11 +287,11 @@ public class FolderIcon extends FrameLayout implements FolderListener { } DeviceProfile grid = launcher.getDeviceProfile(); - sPreviewSize = grid.folderIconSizePx; + sPreviewSize = grid.iconSizePx; sPreviewPadding = res.getDimensionPixelSize(R.dimen.folder_preview_padding); - sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer); - sSharedInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_nolip); - sSharedFolderLeaveBehind = res.getDrawable(R.drawable.portal_ring_rest); + sSharedOuterRingDrawable = res.getDrawable(R.drawable.folder_fill_highlight); + sSharedInnerRingDrawable = null; + sSharedFolderLeaveBehind = res.getDrawable(R.drawable.folder_bg); sStaticValuesDirty = false; } } @@ -324,14 +391,28 @@ public class FolderIcon extends FrameLayout implements FolderListener { } private boolean willAcceptItem(ItemInfo item) { + if (mInfo.isRemote()) return false; + final int itemType = item.itemType; + + boolean hidden = false; + if (item instanceof FolderInfo){ + if (((FolderInfo) item).isRemote()) return false; + + hidden = ((FolderInfo) item).hidden; + } return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || - itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && - !mFolder.isFull() && item != mInfo && !mInfo.opened); + itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT || + itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) && + !mFolder.isFull() && item != mInfo && !mInfo.opened && + !hidden); } public boolean acceptDrop(Object dragInfo) { final ItemInfo item = (ItemInfo) dragInfo; + if (mInfo.hidden) { + return false; + } return !mFolder.isDestroyed() && willAcceptItem(item); } @@ -370,12 +451,21 @@ public class FolderIcon extends FrameLayout implements FolderListener { item = ((AppInfo) mDragInfo).makeShortcut(); item.spanX = 1; item.spanY = 1; + } else if (mDragInfo instanceof FolderInfo) { + return; } else { // ShortcutInfo item = (ShortcutInfo) mDragInfo; } mFolder.beginExternalDrag(item); - mLauncher.openFolder(FolderIcon.this); + mFolderRingAnimator.mCellLayout.hideFolderAccept(mFolderRingAnimator); + + int[] folderTouchXY = new int[2]; + mFolder.getLocationOnScreen(folderTouchXY); + int[] folderTouchXYOffset = {folderTouchXY[0] + mFolder.getWidth() / 2, + folderTouchXY[1] + mFolder.getHeight() / 2}; + + mLauncher.openFolder(FolderIcon.this, folderTouchXYOffset); } }; @@ -482,6 +572,15 @@ public class FolderIcon extends FrameLayout implements FolderListener { if (d.dragInfo instanceof AppInfo) { // Came from all apps -- make a copy item = ((AppInfo) d.dragInfo).makeShortcut(); + } else if (d.dragInfo instanceof FolderInfo) { + FolderInfo folder = (FolderInfo) d.dragInfo; + mFolder.notifyDrop(); + for (ShortcutInfo fItem : folder.contents) { + onDrop(fItem, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable, d); + } + mLauncher.removeFolder(folder); + LauncherModel.deleteItemFromDatabase(mLauncher, folder); + return; } else { item = (ShortcutInfo) d.dragInfo; } @@ -511,7 +610,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { mMaxPerspectiveShift = mBaselineIconSize * PERSPECTIVE_SHIFT_FACTOR; mPreviewOffsetX = (mTotalWidth - mAvailableSpaceInPreview) / 2; - mPreviewOffsetY = previewPadding + grid.folderBackgroundOffset; + mPreviewOffsetY = grid.folderBackgroundOffset; } } @@ -605,7 +704,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { super.dispatchDraw(canvas); if (mFolder == null) return; - if (mFolder.getItemCount() == 0 && !mAnimating) return; + if (mFolder.getItemCount() == 0 && !mAnimating && !mInfo.isRemote()) return; ArrayList<View> items = mFolder.getItemsInReadingOrder(); Drawable d; @@ -614,21 +713,65 @@ public class FolderIcon extends FrameLayout implements FolderListener { // Update our drawing parameters if necessary if (mAnimating) { computePreviewDrawingParams(mAnimParams.drawable); - } else { + } else if (!items.isEmpty()) { v = (TextView) items.get(0); d = getTopDrawable(v); - computePreviewDrawingParams(d); + if (d != null) computePreviewDrawingParams(d); + } + + int ntemsInPreview = Math.min(items.size(), NUM_ITEMS_IN_PREVIEW); + + // Hidden folder - don't display Preview + View folderLock = findViewById(R.id.folder_lock_image); + folderLock.setVisibility(mInfo.hidden ? VISIBLE : INVISIBLE); + View appView = findViewById(R.id.app_0); + appView.setVisibility(mInfo.hidden ? INVISIBLE : VISIBLE); + appView = findViewById(R.id.app_1); + appView.setVisibility(mInfo.hidden ? INVISIBLE : VISIBLE); + appView = findViewById(R.id.app_2); + appView.setVisibility(mInfo.hidden ? INVISIBLE : VISIBLE); + appView = findViewById(R.id.app_3); + appView.setVisibility(mInfo.hidden ? INVISIBLE : VISIBLE); + + if (mInfo.hidden) { + return; } - int nItemsInPreview = Math.min(items.size(), NUM_ITEMS_IN_PREVIEW); if (!mAnimating) { - for (int i = nItemsInPreview - 1; i >= 0; i--) { - v = (TextView) items.get(i); - if (!mHiddenItems.contains(v.getTag())) { - d = getTopDrawable(v); + for (int i = 0; i < NUM_ITEMS_IN_PREVIEW; i++) { + d = null; + if (mInfo.isRemote()) { + d = mLauncher.getRemoteFolderManager().getFolderIconDrawable(items, i); + } else if (i < items.size()) { + v = (TextView) items.get(i); + if (!mHiddenItems.contains(v.getTag())) { + d = getTopDrawable(v); + } + } + + if (d != null) { mParams = computePreviewItemDrawingParams(i, mParams); mParams.drawable = d; - drawPreviewItem(canvas, mParams); + } + + ImageView appIcon = null; + switch(i) { + case 0: + appIcon = (ImageView) findViewById(R.id.app_0); + break; + case 1: + appIcon = (ImageView) findViewById(R.id.app_1); + break; + case 2: + appIcon = (ImageView) findViewById(R.id.app_2); + break; + case 3: + appIcon = (ImageView) findViewById(R.id.app_3); + break; + } + + if (appIcon != null) { + appIcon.setImageDrawable(d); } } } else { @@ -645,10 +788,9 @@ public class FolderIcon extends FrameLayout implements FolderListener { final Runnable onCompleteRunnable) { final PreviewItemDrawingParams finalParams = computePreviewItemDrawingParams(0, null); - float iconSize = mLauncher.getDeviceProfile().iconSizePx; - final float scale0 = iconSize / d.getIntrinsicWidth() ; - final float transX0 = (mAvailableSpaceInPreview - iconSize) / 2; - final float transY0 = (mAvailableSpaceInPreview - iconSize) / 2 + getPaddingTop(); + final float scale0 = 1.0f; + final float transX0 = (mAvailableSpaceInPreview - d.getIntrinsicWidth()) / 2; + final float transY0 = (mAvailableSpaceInPreview - d.getIntrinsicHeight()) / 2 + getPaddingTop(); mAnimParams.drawable = d; ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1.0f); @@ -675,7 +817,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { public void onAnimationEnd(Animator animation) { mAnimating = false; if (onCompleteRunnable != null) { - onCompleteRunnable.run(); + mLauncher.runOnUiThread(onCompleteRunnable); } } }); @@ -710,6 +852,18 @@ public class FolderIcon extends FrameLayout implements FolderListener { requestLayout(); } + @Override + public void onRemoveAll() { + invalidate(); + requestLayout(); + } + + @Override + public void onRemoveAll(ArrayList<ShortcutInfo> items) { + invalidate(); + requestLayout(); + } + public void onTitleChanged(CharSequence title) { mFolderName.setText(title); setContentDescription(String.format(getContext().getString(R.string.folder_name_format), diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java index aea21c95b..7969d627b 100644 --- a/src/com/android/launcher3/FolderInfo.java +++ b/src/com/android/launcher3/FolderInfo.java @@ -28,6 +28,7 @@ import java.util.Arrays; * Represents a folder containing shortcuts or apps. */ public class FolderInfo extends ItemInfo { + public static final int REMOTE_SUBTYPE = 1; public static final int NO_FLAGS = 0x00000000; @@ -50,13 +51,15 @@ public class FolderInfo extends ItemInfo { * Whether this folder has been opened */ boolean opened; + int subType; public int options; /** - * The apps and shortcuts + * The apps and shortcuts and hidden status */ public ArrayList<ShortcutInfo> contents = new ArrayList<ShortcutInfo>(); + public Boolean hidden = false; ArrayList<FolderListener> listeners = new ArrayList<FolderListener>(); @@ -91,6 +94,54 @@ public class FolderInfo extends ItemInfo { itemsChanged(); } + /** + * Remove all apps and shortcuts. Does not change the DB unless + * LauncherModel.deleteFolderContentsFromDatabase(Context, FolderInfo) is called first. + */ + public void removeAll() { + if (contents.isEmpty()) return; + + contents.clear(); + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onRemoveAll(); + } + itemsChanged(); + } + + /** + * Remove all supplied shortcuts. Does not change the DB unless + * LauncherModel.deleteFolderContentsFromDatabase(Context, FolderInfo) is called first. + * @param items the shortcuts to remove. + */ + public void removeAll(ArrayList<ShortcutInfo> items) { + if (items.isEmpty()) return; + + contents.removeAll(items); + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onRemoveAll(items); + } + itemsChanged(); + } + + /** + * @return true if this info represents a remote folder, false otherwise + */ + public boolean isRemote() { + return (subType & REMOTE_SUBTYPE) != 0; + } + + /** + * Set flag indicating whether this folder is remote + * @param remote true if folder is remote, false otherwise + */ + public void setRemote(final boolean remote) { + if (remote) { + subType |= REMOTE_SUBTYPE; + } else { + subType &= ~REMOTE_SUBTYPE; + } + } + public void setTitle(CharSequence title) { this.title = title; for (int i = 0; i < listeners.size(); i++) { @@ -103,7 +154,8 @@ public class FolderInfo extends ItemInfo { super.onAddToDatabase(context, values); values.put(LauncherSettings.Favorites.TITLE, title.toString()); values.put(LauncherSettings.Favorites.OPTIONS, options); - + values.put(LauncherSettings.Favorites.HIDDEN, hidden ? 1 : 0); + values.put(LauncherSettings.BaseLauncherColumns.SUBTYPE, subType); } void addListener(FolderListener listener) { @@ -129,18 +181,21 @@ public class FolderInfo extends ItemInfo { } interface FolderListener { - public void onAdd(ShortcutInfo item); - public void onRemove(ShortcutInfo item); - public void onTitleChanged(CharSequence title); - public void onItemsChanged(); + void onAdd(ShortcutInfo item); + void onRemove(ShortcutInfo item); + void onRemoveAll(); + void onRemoveAll(ArrayList<ShortcutInfo> items); + void onTitleChanged(CharSequence title); + void onItemsChanged(); } @Override public String toString() { - return "FolderInfo(id=" + this.id + " type=" + this.itemType + return "FolderInfo(id=" + this.id + " type=" + this.itemType + " subtype=" + this.subType + " container=" + this.container + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX - + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + ")"; + + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + + " hidden=" + hidden + ")"; } public boolean hasOption(int optionFlag) { diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java index cc9c5738a..5d5ac3d5a 100644 --- a/src/com/android/launcher3/FolderPagedView.java +++ b/src/com/android/launcher3/FolderPagedView.java @@ -30,6 +30,7 @@ import android.view.animation.OvershootInterpolator; import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener; import com.android.launcher3.PageIndicator.PageMarkerResources; import com.android.launcher3.Workspace.ItemOperator; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -113,7 +114,7 @@ public class FolderPagedView extends PagedView { * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while * maintaining the restrictions of {@link #mMaxCountX} & {@link #mMaxCountY}. */ - private void setupContentDimensions(int count) { + public void setupContentDimensions(int count) { mAllocatedContentSize = count; boolean done; if (count >= mMaxItemsPerPage) { @@ -214,6 +215,11 @@ public class FolderPagedView extends PagedView { textView.setOnClickListener(mFolder); textView.setOnLongClickListener(mFolder); textView.setOnFocusChangeListener(mFocusIndicatorView); + if (SettingsProvider.getBoolean(mFolder.mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default)) { + textView.setTextVisibility(false); + } textView.setOnKeyListener(mKeyListener); textView.setLayoutParams(new CellLayout.LayoutParams( @@ -268,6 +274,51 @@ public class FolderPagedView extends PagedView { } } + public void removeAllItems() { + for (int i = 0; i < getChildCount(); i++) { + getPageAt(i).removeAllViews(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (getChildCount() == 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } else { + // We should only be as large as our pages, so measure all of them first. + View page = null; + for (int i = 0; i < getChildCount(); i++) { + page = getChildAt(i); + page.measure(widthMeasureSpec, heightMeasureSpec); + } + + // And then set ourselves to their size. + int width = getPaddingLeft() + page.getMeasuredWidth() + getPaddingRight(); + int height = getPaddingTop() + page.getMeasuredHeight() + getPaddingBottom(); + mViewport.set(0, 0, width, height); + setMeasuredDimension(width, height); + } + } + + /** + * Find the child view for the given rank. + * @param rank sorted index of child. + * @return view of child at given rank. + */ + public View getChildAtRank(int rank) { + int pagePos = rank % mMaxItemsPerPage; + int pageNo = rank / mMaxItemsPerPage; + int cellX = pagePos % mGridCountX; + int cellY = pagePos / mGridCountX; + + CellLayout page = getPageAt(pageNo); + if (page != null) { + return page.getChildAt(cellX, cellY); + } else { + return null; + } + } + /** * Updates position and rank of all the children in the view. * It essentially removes all views from all the pages and then adds them again in appropriate diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 59ab8397d..ca5545df0 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -29,6 +29,7 @@ import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteReadOnlyDatabaseException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; @@ -52,8 +53,10 @@ import com.android.launcher3.util.Thunk; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.Stack; @@ -92,7 +95,7 @@ public class IconCache { private final HashMap<ComponentKey, CacheEntry> mCache = new HashMap<ComponentKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); private final int mIconDpi; - @Thunk final IconDB mIconDb; + @Thunk IconDB mIconDb; @Thunk final Handler mWorkerHandler; @@ -198,6 +201,31 @@ public class IconCache { } /** + * Empty out the cache. + */ + public synchronized void flush() { + mCache.clear(); + if (mIconDb != null) { + mIconDb.close(); + } + mIconDb = new IconDB(mContext); + } + + /** + * Empty out the cache that aren't of the correct grid size + */ + public synchronized void flushInvalidIcons(DeviceProfile deviceProfile) { + Iterator<Map.Entry<ComponentKey, CacheEntry>> it = mCache.entrySet().iterator(); + while (it.hasNext()) { + final CacheEntry e = it.next().getValue(); + if ((e.icon != null) && (e.icon.getWidth() < deviceProfile.iconSizePx + || e.icon.getHeight() < deviceProfile.iconSizePx)) { + it.remove(); + } + } + } + + /** * Remove any records for the supplied package name from memory. */ private void removeFromMemCacheLocked(String packageName, UserHandleCompat user) { @@ -363,8 +391,12 @@ public class IconCache { values.put(IconDB.COLUMN_USER, userSerial); values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime); values.put(IconDB.COLUMN_VERSION, info.versionCode); - mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, - SQLiteDatabase.CONFLICT_REPLACE); + try { + mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } catch (SQLiteReadOnlyDatabaseException e) { + Log.e(TAG, "Can't add icon to read only db", e); + } } @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app, diff --git a/src/com/android/launcher3/InsettableFrameLayout.java b/src/com/android/launcher3/InsettableFrameLayout.java index 7343bf686..770d1de1a 100644 --- a/src/com/android/launcher3/InsettableFrameLayout.java +++ b/src/com/android/launcher3/InsettableFrameLayout.java @@ -24,10 +24,14 @@ public class InsettableFrameLayout extends FrameLayout implements if (child instanceof Insettable) { ((Insettable) child).setInsets(newInsets); } else if (!lp.ignoreInsets) { - lp.topMargin += (newInsets.top - oldInsets.top); + if (!lp.ignoreTopInsets) { + lp.topMargin += (newInsets.top - oldInsets.top); + } lp.leftMargin += (newInsets.left - oldInsets.left); lp.rightMargin += (newInsets.right - oldInsets.right); - lp.bottomMargin += (newInsets.bottom - oldInsets.bottom); + if (!lp.ignoreBottomInsets) { + lp.bottomMargin += (newInsets.bottom - oldInsets.bottom); + } } child.setLayoutParams(lp); } @@ -65,6 +69,8 @@ public class InsettableFrameLayout extends FrameLayout implements public static class LayoutParams extends FrameLayout.LayoutParams { boolean ignoreInsets = false; + boolean ignoreTopInsets = false; + boolean ignoreBottomInsets = false; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); @@ -72,6 +78,10 @@ public class InsettableFrameLayout extends FrameLayout implements R.styleable.InsettableFrameLayout_Layout); ignoreInsets = a.getBoolean( R.styleable.InsettableFrameLayout_Layout_layout_ignoreInsets, false); + ignoreTopInsets = a.getBoolean( + R.styleable.InsettableFrameLayout_Layout_layout_ignoreTopInsets, false); + ignoreBottomInsets = a.getBoolean( + R.styleable.InsettableFrameLayout_Layout_layout_ignoreBottomInsets, false); a.recycle(); } @@ -91,6 +101,7 @@ public class InsettableFrameLayout extends FrameLayout implements @Override public void onChildViewRemoved(View parent, View child) { + setFrameLayoutChildInsets(child, new Rect(), mInsets); } } diff --git a/src/com/android/launcher3/InsettableLinearLayout.java b/src/com/android/launcher3/InsettableLinearLayout.java new file mode 100644 index 000000000..8f64713e5 --- /dev/null +++ b/src/com/android/launcher3/InsettableLinearLayout.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2015 The CyanogenMod 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; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +public class InsettableLinearLayout extends LinearLayout implements + ViewGroup.OnHierarchyChangeListener, Insettable { + + protected Rect mInsets = new Rect(); + + public InsettableLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + setOnHierarchyChangeListener(this); + } + + public void setLinearLayoutChildInsets(View child, Rect newInsets, Rect oldInsets) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (child instanceof Insettable) { + ((Insettable) child).setInsets(newInsets); + } else if (!lp.ignoreInsets) { + if (!lp.ignoreTopInsets) { + lp.topMargin += (newInsets.top - oldInsets.top); + } + lp.leftMargin += (newInsets.left - oldInsets.left); + lp.rightMargin += (newInsets.right - oldInsets.right); + if (!lp.ignoreBottomInsets) { + lp.bottomMargin += (newInsets.bottom - oldInsets.bottom); + } + } + child.setLayoutParams(lp); + } + + @Override + public void setInsets(Rect insets) { + final int n = getChildCount(); + for (int i = 0; i < n; i++) { + final View child = getChildAt(i); + setLinearLayoutChildInsets(child, insets, mInsets); + } + mInsets.set(insets); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + // Override to allow type-checking of LayoutParams. + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + public static class LayoutParams extends LinearLayout.LayoutParams { + boolean ignoreInsets = false; + boolean ignoreTopInsets = false; + boolean ignoreBottomInsets = false; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + TypedArray a = c.obtainStyledAttributes(attrs, + R.styleable.InsettableLinearLayout_Layout); + ignoreInsets = a.getBoolean( + R.styleable.InsettableLinearLayout_Layout_layout_ignoreInsets, false); + ignoreTopInsets = a.getBoolean( + R.styleable.InsettableLinearLayout_Layout_layout_ignoreTopInsets, false); + ignoreBottomInsets = a.getBoolean( + R.styleable.InsettableLinearLayout_Layout_layout_ignoreBottomInsets, false); + a.recycle(); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(ViewGroup.LayoutParams lp) { + super(lp); + } + } + + @Override + public void onChildViewAdded(View parent, View child) { + setLinearLayoutChildInsets(child, mInsets, new Rect()); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + } + +} diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index ae204c40c..e21a070ad 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -24,6 +24,7 @@ import android.util.DisplayMetrics; import android.view.Display; import android.view.WindowManager; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -32,6 +33,38 @@ import java.util.Comparator; public class InvariantDeviceProfile { + public enum GridSize { + Comfortable(0), + Cozy(1), + Condensed(2), + Custom(3); + + private final int mValue; + GridSize(int value) { + mValue = value; + } + + public int getValue() { + return mValue; + } + + public static GridSize getModeForValue(int value) { + switch (value) { + case 1: + return Cozy; + case 2: + return Condensed; + case 3: + return Custom; + default : + return Comfortable; + } + } + } + + public final static int GRID_SIZE_MAX = 3; + public final static int GRID_SIZE_MIN = 2; + // This is a static that we use for the default icon size on a 4/5-inch phone private static float DEFAULT_ICON_SIZE_DP = 60; @@ -55,6 +88,8 @@ public class InvariantDeviceProfile { */ public int numRows; public int numColumns; + public int numRowsBase; + public int numColumnsBase; /** * The minimum number of predicted apps in all apps. @@ -132,7 +167,8 @@ public class InvariantDeviceProfile { minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); ArrayList<InvariantDeviceProfile> closestProfiles = - findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles()); + findClosestDeviceProfiles(minWidthDps, minHeightDps, + getPredefinedDeviceProfiles(context)); InvariantDeviceProfile interpolatedDeviceProfileOut = invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles); @@ -146,6 +182,29 @@ public class InvariantDeviceProfile { numFolderColumns = closestProfile.numFolderColumns; minAllAppsPredictionColumns = closestProfile.minAllAppsPredictionColumns; + numRowsBase = numRows; + int gridResize = SettingsProvider.getIntCustomDefault(context, + SettingsProvider.SETTINGS_UI_DYNAMIC_GRID_SIZE, 0); + if (GridSize.getModeForValue(gridResize) != GridSize.Custom) { + numRows += gridResize; + } else { + int iTempNumberOfRows = SettingsProvider.getIntCustomDefault(context, + SettingsProvider.SETTINGS_UI_HOMESCREEN_ROWS, numRows); + if (iTempNumberOfRows > 0) { + numRows = iTempNumberOfRows; + } + } + numColumnsBase = numColumns; + if (GridSize.getModeForValue(gridResize) != GridSize.Custom) { + numColumns += gridResize; + } else { + int iTempNumberOfColumns = SettingsProvider.getIntCustomDefault(context, + SettingsProvider.SETTINGS_UI_HOMESCREEN_COLUMNS, numColumns); + if (iTempNumberOfColumns > 0) { + numColumns = iTempNumberOfColumns; + } + } + iconSize = interpolatedDeviceProfileOut.iconSize; iconBitmapSize = Utilities.pxFromDp(iconSize, dm); iconTextSize = interpolatedDeviceProfileOut.iconTextSize; @@ -169,35 +228,52 @@ public class InvariantDeviceProfile { smallSide, largeSide, false /* isLandscape */); } - ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles() { + ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles(Context context) { + boolean useLargeIcons = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_GENERAL_ICONS_LARGE, + R.bool.preferences_interface_general_icons_large_default); ArrayList<InvariantDeviceProfile> predefinedDeviceProfiles = new ArrayList<>(); // width, height, #rows, #columns, #folder rows, #folder columns, // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId. predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby", - 255, 300, 2, 3, 2, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + 255, 300, 2, 3, 2, 3, 3, (useLargeIcons? 58 : 46), 13, 3, + (useLargeIcons? 58 : 46), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby", - 255, 400, 3, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + 255, 400, 3, 3, 3, 3, 3, (useLargeIcons? 58 : 46), 13, 3, + (useLargeIcons? 58 : 46), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby", - 275, 420, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + 275, 420, 3, 4, 3, 4, 4, (useLargeIcons? 58 : 46), 13, 5, + (useLargeIcons? 58 : 46), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby", - 255, 450, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + 255, 450, 3, 4, 3, 4, 4, (useLargeIcons? 58 : 46), 13, 5, + (useLargeIcons? 58 : 46), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S", - 296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + 296, 491.33f, 4, 4, 4, 4, 4, (useLargeIcons? 58 : 46), 13, 5, + (useLargeIcons? 58 : 46), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4", - 335, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + 335, 567, 4, 4, 4, 4, 4, (useLargeIcons ? DEFAULT_ICON_SIZE_DP : 52), 13, 5, + (useLargeIcons? 60 : 50), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5", - 359, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + 359, 567, 4, 4, 4, 4, 4, (useLargeIcons ? DEFAULT_ICON_SIZE_DP : 52), 13, 5, + (useLargeIcons? 60 : 50), R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Bacon", + 336, 592, 4, 4, 4, 4, 4, (useLargeIcons ? DEFAULT_ICON_SIZE_DP : 52), 13, 5, + (useLargeIcons? 60 : 48), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone", - 406, 694, 5, 5, 4, 4, 4, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); + 406, 694, 5, 5, 4, 4, 4, (useLargeIcons ? 68 : 56), 14.4f, 5, + (useLargeIcons ? 60 : 48), R.xml.default_workspace_5x5)); // The tablet profile is odd in that the landscape orientation // also includes the nav bar on the side predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7", - 575, 904, 5, 6, 4, 5, 4, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); + 575, 904, 5, 6, 4, 5, 4, (useLargeIcons ? 76 : 60), 14.4f, 7, + (useLargeIcons ? 60 : 48), R.xml.default_workspace_5x6)); // Larger tablet profiles always have system bars on the top & bottom predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10", - 727, 1207, 5, 6, 4, 5, 4, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); + 727, 1207, 5, 6, 4, 5, 4, (useLargeIcons ? 80 : 64), 14.4f, 7, + (useLargeIcons ? 68 : 56), R.xml.default_workspace_5x6)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet", - 1527, 2527, 7, 7, 6, 6, 4, 100, 20, 7, 72, R.xml.default_workspace_4x4)); + 1527, 2527, 7, 7, 6, 6, 4, (useLargeIcons ? 104 : 80), 20, 7, + (useLargeIcons ? 76 : 64), R.xml.default_workspace_4x4)); return predefinedDeviceProfiles; } diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java index f7e0ea488..b30c954f5 100644 --- a/src/com/android/launcher3/ItemInfo.java +++ b/src/com/android/launcher3/ItemInfo.java @@ -20,6 +20,7 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; @@ -115,12 +116,24 @@ public class ItemInfo { public CharSequence contentDescription; /** + * Indicates that this item has had it's position changed + * because the grid size was made smaller and it could no longer fit. + */ + public boolean wasMovedDueToReducedSpace = false; + + /** * The position of the item in a drag-and-drop operation. */ public int[] dropPos = null; public UserHandleCompat user; + /** + * A custom drawable to use for the icon. Not persisted to the database because + * it is not guaranteed to be a bitmap (could be a vector). + */ + Drawable customDrawable; + public ItemInfo() { user = UserHandleCompat.myUserHandle(); } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 1f843cb70..b135fefd9 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -16,6 +16,7 @@ package com.android.launcher3; +import android.Manifest; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -28,6 +29,9 @@ import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.AlertDialog; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; import android.app.SearchManager; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; @@ -53,6 +57,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.Rect; +import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -103,7 +108,10 @@ import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.list.SettingsPinnedHeaderAdapter; import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.settings.SettingsProvider; +import com.android.launcher3.stats.LauncherStats; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.LongArrayMap; import com.android.launcher3.util.Thunk; @@ -150,12 +158,22 @@ public class Launcher extends Activity private static final int REQUEST_BIND_APPWIDGET = 11; private static final int REQUEST_RECONFIGURE_APPWIDGET = 12; + private static final int REQUEST_PERMISSION_CALL_PHONE = 13; + + public static final int REQUEST_OPEN_PROTECTED_FOLDER = 14; + public static final int REQUEST_PROTECT_FOLDER = 15; + public static final int REQUEST_UNPROTECT_FOLDER = 16; + private static final int WORKSPACE_BACKGROUND_GRADIENT = 0; private static final int WORKSPACE_BACKGROUND_TRANSPARENT = 1; private static final int WORKSPACE_BACKGROUND_BLACK = 2; private static final float BOUNCE_ANIMATION_TENSION = 1.3f; + + public static final String LONGPRESS_CHANGE = "wallpaper_changed_by_longpress"; + + /** * IntentStarter uses request codes starting with this. This must be greater than all activity * request codes used internally. @@ -238,6 +256,9 @@ public class Launcher extends Activity private final BroadcastReceiver mCloseSystemDialogsReceiver = new CloseSystemDialogsIntentReceiver(); + @Thunk final NetworkConnectionReceiver mConnectionReceiver = new NetworkConnectionReceiver(); + @Thunk final DeviceUnlockedReceiver mDeviceUnlockedReceiver = new DeviceUnlockedReceiver(); + private LayoutInflater mInflater; @Thunk Workspace mWorkspace; @@ -246,6 +267,9 @@ public class Launcher extends Activity @Thunk DragLayer mDragLayer; private DragController mDragController; private View mWeightWatcher; + private DynamicGridSizeFragment mDynamicGridSizeFragment; + + protected static RemoteFolderManager sRemoteFolderManager; private AppWidgetManagerCompat mAppWidgetManager; private LauncherAppWidgetHost mAppWidgetHost; @@ -256,8 +280,14 @@ public class Launcher extends Activity private int[] mTmpAddItemCellCoordinates = new int[2]; + protected FolderIcon mHiddenFolderIcon; + private boolean mHiddenFolderLockStateChanged = false; + private boolean mHiddenFolderAuth = false; + @Thunk Hotseat mHotseat; - private ViewGroup mOverviewPanel; + private VerticalSlidingPanel mOverviewPanel; + private View mDarkPanel; + OverviewSettingsPanel mOverviewSettingsPanel; private View mAllAppsButton; private View mWidgetsButton; @@ -294,7 +324,7 @@ public class Launcher extends Activity private Bundle mSavedInstanceState; - private LauncherModel mModel; + protected LauncherModel mModel; private IconCache mIconCache; @Thunk boolean mUserPresent = true; private boolean mVisible = false; @@ -345,10 +375,63 @@ public class Launcher extends Activity private DeviceProfile mDeviceProfile; + private boolean mUseScrubber = true; + + private boolean mIsDrawerSearchBarEnabled; + // This is set to the view that launched the activity that navigated the user away from // launcher. Since there is no callback for when the activity has finished launching, enable // the press state and keep this reference to reset the press state when we return to launcher. - private BubbleTextView mWaitingForResume; + BubbleTextView mWaitingForResume; + + private long mDefaultScreenId; + + public Animator.AnimatorListener mAnimatorListener = new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator arg0) {} + @Override + public void onAnimationRepeat(Animator arg0) {} + @Override + public void onAnimationEnd(Animator arg0) { + mDarkPanel.setVisibility(View.GONE); + } + @Override + public void onAnimationCancel(Animator arg0) {} + }; + + Runnable mReloadLauncherRunnable = new Runnable() { + @Override + public void run() { + reloadLauncher(mWorkspace.getRestorePage(), true, false); + } + }; + + private BroadcastReceiver protectedAppsChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Update the workspace + if (waitUntilResume(mReloadLauncherRunnable, true)) { + return; + } + + reloadLauncher(mWorkspace.getRestorePage(), true, false); + } + }; + + private BroadcastReceiver searchBarVisibilityChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Update the workspace + if (waitUntilResume(mReloadLauncherRunnable, true)) { + return; + } + + reloadLauncher(mWorkspace.getRestorePage(), true, false); + } + }; + + // Preferences + private boolean mHideIconLabels; protected static HashMap<String, CustomAppWidget> sCustomAppWidgets = new HashMap<String, CustomAppWidget>(); @@ -425,17 +508,11 @@ public class Launcher extends Activity LauncherAppState.setApplicationContext(getApplicationContext()); LauncherAppState app = LauncherAppState.getInstance(); - // Load configuration-specific DeviceProfile - mDeviceProfile = getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE ? - app.getInvariantDeviceProfile().landscapeProfile - : app.getInvariantDeviceProfile().portraitProfile; + initializeDeviceProfile(app); mSharedPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); mIsSafeModeEnabled = getPackageManager().isSafeMode(); - mModel = app.setLauncher(this); - mIconCache = app.getIconCache(); mDragController = new DragController(this); mInflater = getLayoutInflater(); @@ -448,6 +525,12 @@ public class Launcher extends Activity mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID); mAppWidgetHost.startListening(); + if (sRemoteFolderManager == null) { + sRemoteFolderManager = new RemoteFolderManager(this); + } else { + sRemoteFolderManager.onRecreateLauncher(this); + } + // If we are getting an onCreate, we can actually preempt onResume and unset mPaused here, // this also ensures that any synchronous binding below doesn't re-trigger another // LauncherModel load. @@ -491,6 +574,12 @@ public class Launcher extends Activity IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); registerReceiver(mCloseSystemDialogsReceiver, filter); + filter = new IntentFilter(NetworkConnectionReceiver.INTENT_ACTION); + registerReceiver(mConnectionReceiver, filter); + + filter = new IntentFilter(DeviceUnlockedReceiver.INTENT_ACTION); + registerReceiver(mDeviceUnlockedReceiver, filter); + mRotationEnabled = Utilities.isRotationAllowedForDevice(getApplicationContext()); // In case we are on a device with locked rotation, we should look at preferences to check // if the user has specifically allowed rotation. @@ -519,11 +608,20 @@ public class Launcher extends Activity showFirstRunActivity(); showFirstRunClings(); } + + IntentFilter protectedAppsFilter = new IntentFilter( + cyanogenmod.content.Intent.ACTION_PROTECTED_CHANGED); + registerReceiver(protectedAppsChangedReceiver, protectedAppsFilter, + cyanogenmod.platform.Manifest.permission.PROTECTED_APP, null); + + IntentFilter searchBarVisibilityFilter = new IntentFilter( + SettingsPinnedHeaderAdapter.ACTION_SEARCH_BAR_VISIBILITY_CHANGED); + registerReceiver(searchBarVisibilityChangedReceiver, searchBarVisibilityFilter); } @Override public void onSettingsChanged(String settings, boolean value) { - if (Utilities.ALLOW_ROTATION_PREFERENCE_KEY.equals(settings)) { + if (SettingsProvider.SETTINGS_UI_ALLOW_ROTATION.equals(settings)) { mRotationEnabled = value; if (!waitUntilResume(mUpdateOrientationRunnable, true)) { mUpdateOrientationRunnable.run(); @@ -744,6 +842,7 @@ public class Launcher extends Activity // When the user has granted permission to bind widgets, we should check to see if // we can inflate the default search bar widget. getOrCreateQsbBar(); + showWorkspace(false); } return; } else if (requestCode == REQUEST_PICK_WALLPAPER) { @@ -751,6 +850,30 @@ public class Launcher extends Activity showWorkspace(false); } return; + } else if (requestCode == REQUEST_OPEN_PROTECTED_FOLDER) { + mHiddenFolderAuth = resultCode == RESULT_OK; + if (mHiddenFolderIcon != null && mHiddenFolderAuth) { + openFolder(mHiddenFolderIcon, null); + } else { + mHiddenFolderAuth = false; + } + return; + } else if (requestCode == REQUEST_PROTECT_FOLDER) { + mHiddenFolderAuth = resultCode == RESULT_OK; + if (mHiddenFolderIcon != null && mHiddenFolderAuth) { + mHiddenFolderIcon.getFolder().saveHiddenFolderState(true); + mHiddenFolderLockStateChanged = true; + } else { + mHiddenFolderAuth = false; + } + } else if (requestCode == REQUEST_UNPROTECT_FOLDER) { + mHiddenFolderAuth = resultCode == RESULT_OK; + if (mHiddenFolderIcon != null && mHiddenFolderAuth) { + mHiddenFolderIcon.getFolder().saveHiddenFolderState(false); + mHiddenFolderLockStateChanged = true; + } else { + mHiddenFolderAuth = false; + } } boolean isWidgetDrop = (requestCode == REQUEST_PICK_APPWIDGET || @@ -867,6 +990,24 @@ public class Launcher extends Activity /** @Override for MNC */ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == REQUEST_PERMISSION_CALL_PHONE && sPendingAddItem != null + && sPendingAddItem.requestCode == REQUEST_PERMISSION_CALL_PHONE) { + View v = null; + CellLayout layout = getCellLayout(sPendingAddItem.container, sPendingAddItem.screenId); + if (layout != null) { + v = layout.getChildAt(sPendingAddItem.cellX, sPendingAddItem.cellY); + } + Intent intent = sPendingAddItem.intent; + sPendingAddItem = null; + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startActivity(v, intent, null); + } else { + // TODO: Show a snack bar with link to settings + Toast.makeText(this, getString(R.string.msg_no_phone_permission, + getString(R.string.app_name)), Toast.LENGTH_SHORT).show(); + } + } if (mLauncherCallbacks != null) { mLauncherCallbacks.onRequestPermissionsResult(requestCode, permissions, grantResults); @@ -984,6 +1125,7 @@ public class Launcher extends Activity // switch to another app, otherwise, if it was showAppsView(false /* animated */, false /* resetListToTop */, !launchedFromApp /* updatePredictedApps */, false /* focusSearchBar */); + mWorkspace.setVisibility(View.INVISIBLE); } else if (mOnResumeState == State.WIDGETS) { showWidgetsView(false, false); } @@ -1067,6 +1209,14 @@ public class Launcher extends Activity if (mLauncherCallbacks != null) { mLauncherCallbacks.onResume(); } + + // Close out fragments + Fragment gridFragment = getFragmentManager().findFragmentByTag( + DynamicGridSizeFragment.DYNAMIC_GRID_SIZE_FRAGMENT); + if (gridFragment != null) { + mDynamicGridSizeFragment.setSize(); + unlockScreenOrientation(true); + } } @Override @@ -1368,7 +1518,13 @@ public class Launcher extends Activity mHotseat.setOnLongClickListener(this); } - mOverviewPanel = (ViewGroup) findViewById(R.id.overview_panel); + // Setup the overview panel + mOverviewPanel = (VerticalSlidingPanel) findViewById(R.id.overview_panel); + mOverviewSettingsPanel = new OverviewSettingsPanel(this); + mOverviewSettingsPanel.initializeAdapter(); + + mDarkPanel = mOverviewPanel.findViewById(R.id.dark_panel); + mWidgetsButton = findViewById(R.id.widget_button); mWidgetsButton.setOnClickListener(new OnClickListener() { @Override @@ -1406,6 +1562,35 @@ public class Launcher extends Activity settingsButton.setVisibility(View.GONE); } + View defaultScreenButton = findViewById(R.id.default_screen_button); + defaultScreenButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + if (!mWorkspace.isSwitchingState()) { + onClickDefaultScreenButton(arg0); + } + } + }); + defaultScreenButton.setOnTouchListener(getHapticFeedbackTouchListener()); + + mOverviewPanel.setPanelSlideListener(new SettingsPanelSlideListener()); + mOverviewPanel.setEnableDragViewTouchEvents(true); + + View settingsPaneHeader = mOverviewPanel.findViewById(R.id.settings_pane_header); + if (settingsPaneHeader != null) { + mOverviewPanel.setDragView(settingsPaneHeader); + settingsPaneHeader.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mOverviewPanel.isExpanded()) { + mOverviewPanel.collapsePane(); + } else { + mOverviewPanel.expandPane(); + } + } + }); + } + mOverviewPanel.setAlpha(0f); // Setup the workspace @@ -1421,11 +1606,7 @@ public class Launcher extends Activity // Setup Apps and Widgets mAppsView = (AllAppsContainerView) findViewById(R.id.apps_view); mWidgetsView = (WidgetsContainerView) findViewById(R.id.widgets_view); - if (mLauncherCallbacks != null && mLauncherCallbacks.getAllAppsSearchBarController() != null) { - mAppsView.setSearchBarController(mLauncherCallbacks.getAllAppsSearchBarController()); - } else { - mAppsView.setSearchBarController(mAppsView.newDefaultAppSearchController()); - } + setupSearchBar(this); // Setup the drag controller (drop targets have to be added in reverse order in priority) dragController.setDragScoller(mWorkspace); @@ -1451,6 +1632,8 @@ public class Launcher extends Activity boolean show = shouldShowWeightWatcher(); mWeightWatcher.setVisibility(show ? View.VISIBLE : View.GONE); } + + sRemoteFolderManager.onSetupViews(); } /** @@ -1489,6 +1672,7 @@ public class Launcher extends Activity BubbleTextView favorite = (BubbleTextView) mInflater.inflate(R.layout.app_icon, parent, false); favorite.applyFromShortcutInfo(info, mIconCache); + favorite.setTextVisibility(!mHideIconLabels); favorite.setCompoundDrawablePadding(mDeviceProfile.iconDrawablePaddingPx); favorite.setOnClickListener(this); favorite.setOnFocusChangeListener(mFocusHandler); @@ -1632,6 +1816,161 @@ public class Launcher extends Activity } }; + /** + * Initializes the device profile based off of the launcher app state and screen orientation + * @param app The launcher app state + */ + public void initializeDeviceProfile(LauncherAppState app) { + // Load configuration-specific DeviceProfile + mDeviceProfile = getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE ? + app.getInvariantDeviceProfile().landscapeProfile + : app.getInvariantDeviceProfile().portraitProfile; + + mHideIconLabels = SettingsProvider.getBoolean(this, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + mDefaultScreenId = SettingsProvider.getLongCustomDefault(this, + SettingsProvider.SETTINGS_UI_HOMESCREEN_DEFAULT_SCREEN_ID, + R.integer.preferences_interface_homescreen_id_default); + + mModel = app.setLauncher(this); + mIconCache = app.getIconCache(); + mIconCache.flush(); + } + + /** + * Re-initializes the device profile and layout and reloads the workspace and app drawer as + * appropriate + * @param resizeGrid Indicates whether the grid should be resized + * @param reloadAppDrawer Indicates whether the app drawer should be reloaded + */ + public void reloadLauncher(boolean resizeGrid, boolean reloadAppDrawer) { + reloadLauncher(mWorkspace.getCurrentPage(), resizeGrid, reloadAppDrawer); + } + + /** + * Re-initializes the device profile and layout and reloads the workspace and app drawer as + * appropriate + * @param page The page to bind to + * @param resizeGrid Indicates whether the grid should be resized + * @param reloadAppDrawer Indicates whether the app drawer should be reloaded + */ + private void reloadLauncher(int page, boolean resizeGrid, boolean reloadAppDrawer) + { + // Re-initialize device profile + LauncherAppState app = LauncherAppState.getInstance(); + app.initInvariantDeviceProfile(); + initializeDeviceProfile(app); + + mDeviceProfile.layout(this); + + // Reload + mModel.resetLoadedState(true, true); + int flag = resizeGrid ? LauncherModel.LOADER_FLAG_RESIZE_GRID : + LauncherModel.LOADER_FLAG_NONE; + mModel.startLoader(page, flag); + + mWorkspace.updateCustomContentVisibility(); + + mSearchDropTargetBar.setupQsb(this); + + if (reloadAppDrawer) { + reloadAppDrawer(); + } + } + + public void reloadAppDrawer() { + List<AppInfo> addedApps = mAppsView.getApps(); + mDragLayer.removeView(mAppsView); + mAppsView = (AllAppsContainerView)LayoutInflater + .from(this).inflate(R.layout.all_apps, mDragLayer, false); + mDragLayer.addView(mAppsView, mDragLayer.getChildCount() - 1); + mAppsView.setVisibility(View.INVISIBLE); + setupSearchBar(this); + mAppsView.addApps(addedApps); + tryAndUpdatePredictedApps(); + mAppsView.onReloadAppDrawer(); + sRemoteFolderManager.onReloadAppDrawer(); + } + + public void reloadWidgetView() { + WidgetsModel model = mWidgetsView.getWidgets(); + mDragLayer.removeView(mWidgetsView); + mWidgetsView = (WidgetsContainerView)LayoutInflater + .from(this).inflate(R.layout.widgets_view, mDragLayer, false); + mDragLayer.addView(mWidgetsView, mDragLayer.getChildCount() - 2); + mWidgetsView.setVisibility(View.INVISIBLE); + if (model != null) { + mWidgetsView.addWidgets(model); + } + mWidgetsView.reset(); + } + + /** + * Replaces currently added fragments in the launcher layout with a + * {@link DynamicGridSizeFragment}. + */ + public void onClickDynamicGridSizeButton() { + FragmentManager fragmentManager = getFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + lockScreenOrientation(); + mDynamicGridSizeFragment = new DynamicGridSizeFragment(); + fragmentTransaction.replace(R.id.launcher, mDynamicGridSizeFragment, + DynamicGridSizeFragment.DYNAMIC_GRID_SIZE_FRAGMENT); + fragmentTransaction.commit(); + } + + /** + * If the new grid size is different from the current grid size, the launcher will be reloaded + * and the overview settings panel updated with the new grid size value. + * @param size The new grid size to set the workspace to. + */ + public void setDynamicGridSize(InvariantDeviceProfile.GridSize size) { + int gridSize = SettingsProvider.getIntCustomDefault(this, + SettingsProvider.SETTINGS_UI_DYNAMIC_GRID_SIZE, 0); + boolean customValuesChanged = false; + if (gridSize == size.getValue() && size == InvariantDeviceProfile.GridSize.Custom) { + int tempRows = SettingsProvider.getIntCustomDefault(this, + SettingsProvider.SETTINGS_UI_HOMESCREEN_ROWS, mDeviceProfile.inv.numRows); + int tempColumns = SettingsProvider.getIntCustomDefault(this, + SettingsProvider.SETTINGS_UI_HOMESCREEN_COLUMNS, mDeviceProfile.inv.numColumns); + if (tempColumns != mDeviceProfile.inv.numColumns || + tempRows != mDeviceProfile.inv.numRows) { + customValuesChanged = true; + } + } + + if (gridSize != size.getValue() || customValuesChanged) { + SettingsProvider.putInt(this, + SettingsProvider.SETTINGS_UI_DYNAMIC_GRID_SIZE, size.getValue()); + reloadLauncher(false, true); + } + + // Must be called after reload and before settings invalidation. + sRemoteFolderManager.onGridSizeChanged(); + + mOverviewSettingsPanel.notifyDataSetInvalidated(); + + FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); + Configuration config = getResources().getConfiguration(); + if(config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + fragmentTransaction + .setCustomAnimations(0, R.anim.exit_out_left); + } else { + fragmentTransaction + .setCustomAnimations(0, R.anim.exit_out_right); + } + fragmentTransaction + .remove(mDynamicGridSizeFragment).commit(); + unlockScreenOrientation(true); + mDarkPanel.setVisibility(View.VISIBLE); + ObjectAnimator anim = ObjectAnimator.ofFloat( + mDarkPanel, "alpha", 0.3f, 0.0f); + anim.start(); + anim.addListener(mAnimatorListener); + } + @Override public void onAttachedToWindow() { super.onAttachedToWindow(); @@ -1794,6 +2133,9 @@ public class Launcher extends Activity public void removeAppWidget(LauncherAppWidgetInfo launcherInfo) { removeWidgetToAutoAdvance(launcherInfo.hostView); launcherInfo.hostView = null; + AppWidgetProviderInfo info = mAppWidgetManager.getAppWidgetInfo(launcherInfo.appWidgetId); + String packageName = info.providerInfo.packageName; + LauncherApplication.getLauncherStats().sendWidgetRemoveEvent(packageName); } public void showOutOfSpaceMessage(boolean isHotseatLayout) { @@ -1817,14 +2159,26 @@ public class Launcher extends Activity return mWorkspace; } + public RemoteFolderManager getRemoteFolderManager() { + return sRemoteFolderManager; + } + public Hotseat getHotseat() { return mHotseat; } + public View getPageIndicator() { + return mPageIndicators; + } + public ViewGroup getOverviewPanel() { return mOverviewPanel; } + public View getDarkPanel() { + return mDarkPanel; + } + public SearchDropTargetBar getSearchDropTargetBar() { return mSearchDropTargetBar; } @@ -1955,7 +2309,9 @@ public class Launcher extends Activity outState.putInt(RUNTIME_STATE, mState.ordinal()); // We close any open folder since it will not be re-opened, and we need to make sure // this state is reflected. - closeFolder(); + if (mHiddenFolderIcon == null) { + closeFolder(); + } if (mPendingAddInfo.container != ItemInfo.NO_ID && mPendingAddInfo.screenId > -1 && mWaitingForResult) { @@ -2009,6 +2365,8 @@ public class Launcher extends Activity TextKeyListener.getInstance().release(); unregisterReceiver(mCloseSystemDialogsReceiver); + unregisterReceiver(mConnectionReceiver); + unregisterReceiver(mDeviceUnlockedReceiver); mDragLayer.clearAllResizeFrames(); ((ViewGroup) mWorkspace.getParent()).removeAllViews(); @@ -2021,12 +2379,32 @@ public class Launcher extends Activity if (mLauncherCallbacks != null) { mLauncherCallbacks.onDestroy(); } + + unregisterReceiver(protectedAppsChangedReceiver); + unregisterReceiver(searchBarVisibilityChangedReceiver); } public DragController getDragController() { return mDragController; } + public void validateLockForHiddenFolders(FolderIcon info, int action) { + mHiddenFolderIcon = info; + // Validate Lock Pattern + Intent lockPatternActivity = new Intent(); + lockPatternActivity.setClassName( + "com.android.settings", + "com.android.settings.applications.LockPatternActivity"); + startActivityForResult(lockPatternActivity, action); + mHiddenFolderAuth = false; + } + + public void notifyFolderNameChanged() { + // Reload + mModel.resetLoadedState(true, true); + mModel.startLoader(mWorkspace.getCurrentPage(), LauncherModel.LOADER_FLAG_NONE); + } + @Override public void startActivityForResult(Intent intent, int requestCode) { onStartForResult(requestCode); @@ -2159,7 +2537,7 @@ public class Launcher extends Activity super.onPrepareOptionsMenu(menu); if (!isOnCustomContent()) { // Close any open folders - closeFolder(); + closeFolder(false); // Stop resizing any widgets mWorkspace.exitWidgetResizeMode(); if (!mWorkspace.isInOverviewMode()) { @@ -2251,6 +2629,8 @@ public class Launcher extends Activity completeAddAppWidget(appWidgetId, info.container, info.screenId, boundWidget, appWidgetInfo); mWorkspace.removeExtraEmptyScreenDelayed(true, onComplete, delay, false); + String packageName = appWidgetInfo.providerInfo.packageName; + LauncherApplication.getLauncherStats().sendWidgetAddEvent(packageName); } } @@ -2361,10 +2741,14 @@ public class Launcher extends Activity } FolderIcon addFolder(CellLayout layout, long container, final long screenId, int cellX, - int cellY) { - final FolderInfo folderInfo = new FolderInfo(); + int cellY) { + FolderInfo folderInfo = new FolderInfo(); folderInfo.title = getText(R.string.folder_name); + return addFolder(layout, container, screenId, cellX, cellY, folderInfo); + } + FolderIcon addFolder(CellLayout layout, long container, final long screenId, int cellX, + int cellY, FolderInfo folderInfo) { // Update the model LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screenId, cellX, cellY); @@ -2373,6 +2757,9 @@ public class Launcher extends Activity // Create the view FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, layout, folderInfo, mIconCache); + if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + newFolder.setTextVisible(!mHideIconLabels); + } mWorkspace.addInScreen(newFolder, container, screenId, cellX, cellY, 1, 1, isWorkspaceLocked()); // Force measure the new folder icon @@ -2424,7 +2811,18 @@ public class Launcher extends Activity } else if (isWidgetsViewVisible()) { showOverviewMode(true); } else if (mWorkspace.isInOverviewMode()) { - showWorkspace(true); + Fragment gridFragment = getFragmentManager().findFragmentByTag( + DynamicGridSizeFragment.DYNAMIC_GRID_SIZE_FRAGMENT); + if (gridFragment != null) { + mDynamicGridSizeFragment.setSize(); + unlockScreenOrientation(true); + } + else { + showWorkspace(true); + // Background was set to gradient in onPause(), restore to black if in all apps. + setWorkspaceBackground(mState == State.WORKSPACE ? WORKSPACE_BACKGROUND_GRADIENT + : WORKSPACE_BACKGROUND_TRANSPARENT); + } } else if (mWorkspace.getOpenFolder() != null) { Folder openFolder = mWorkspace.getOpenFolder(); if (openFolder.isEditingName()) { @@ -2440,6 +2838,7 @@ public class Launcher extends Activity } } + /** * Re-listen when widget host is reset. */ @@ -2489,7 +2888,15 @@ public class Launcher extends Activity } else if (v == mAllAppsButton) { onClickAllAppsButton(v); } else if (tag instanceof AppInfo) { + AppInfo info = (AppInfo) tag; startAppShortcutOrInfoActivity(v); + LauncherApplication.getLauncherStats().sendAppLaunchEvent( + LauncherStats.ORIGIN_APPDRAWER, info.componentName.getPackageName()); + String packageName = info.getIntent().getComponent().getPackageName(); + if (LauncherStats.SETTINGS_PACKAGE_NAME.equals(packageName)) { + LauncherApplication.getLauncherStats() + .sendSettingsOpenedEvent(LauncherStats.ORIGIN_APPDRAWER); + } } else if (tag instanceof LauncherAppWidgetInfo) { if (v instanceof PendingAppWidgetHostView) { onClickPendingWidget((PendingAppWidgetHostView) v); @@ -2638,6 +3045,17 @@ public class Launcher extends Activity // Start activities startAppShortcutOrInfoActivity(v); + ComponentName componentName = intent.getComponent(); + if (componentName != null) { + String packageName = intent.getComponent().getPackageName(); + LauncherApplication.getLauncherStats() + .sendAppLaunchEvent(LauncherStats.ORIGIN_HOMESCREEN, + packageName); + if (LauncherStats.SETTINGS_PACKAGE_NAME.equals(packageName)) { + LauncherApplication.getLauncherStats().sendSettingsOpenedEvent( + LauncherStats.ORIGIN_HOMESCREEN); + } + } if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickAppShortcut(v); @@ -2688,6 +3106,11 @@ public class Launcher extends Activity final FolderInfo info = folderIcon.getFolderInfo(); Folder openFolder = mWorkspace.getFolderForTag(info); + int[] folderTouchXY = new int[2]; + v.getLocationOnScreen(folderTouchXY); + int[] folderTouchXYOffset = {folderTouchXY[0] + v.getWidth() / 2, + folderTouchXY[1] + v.getHeight() / 2}; + // If the folder info reports that the associated folder is open, then verify that // it is actually opened. There have been a few instances where this gets out of sync. if (info.opened && openFolder == null) { @@ -2700,7 +3123,7 @@ public class Launcher extends Activity // Close any open folder closeFolder(); // Open the requested folder - openFolder(folderIcon); + openFolder(folderIcon, folderTouchXYOffset); } else { // Find the open folder... int folderScreen; @@ -2712,7 +3135,7 @@ public class Launcher extends Activity // Close any folder open on the current screen closeFolder(); // Pull the folder onto this screen - openFolder(folderIcon); + openFolder(folderIcon, folderTouchXYOffset); } } } @@ -2761,7 +3184,33 @@ public class Launcher extends Activity if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickSettingsButton(v); } else { - startActivity(new Intent(this, SettingsActivity.class)); + if (mOverviewPanel.isExpanded()) { + mOverviewPanel.collapsePane(); + } else { + mOverviewPanel.expandPane(); + } + } + } + + protected void onClickDefaultScreenButton(View v) { + if (LOGD) Log.d(TAG, "onClickDefaultScreenButton"); + + if (!mWorkspace.isInOverviewMode()) return; + + mDefaultScreenId = mWorkspace.getScreenIdForPageIndex(mWorkspace.getPageNearestToCenterOfScreen()); + updateDefaultScreenButton(); + SettingsProvider.get(this).edit() + .putLong(SettingsProvider.SETTINGS_UI_HOMESCREEN_DEFAULT_SCREEN_ID, + mDefaultScreenId) + .commit(); + } + + protected void updateDefaultScreenButton() { + if (mOverviewPanel != null) { + View defaultPageButton = mOverviewPanel.findViewById(R.id.default_screen_button); + defaultPageButton.setActivated( + mWorkspace.getScreenIdForPageIndex(mWorkspace.getPageNearestToCenterOfScreen()) + == mDefaultScreenId); } } @@ -2869,8 +3318,9 @@ public class Launcher extends Activity } private boolean startActivity(View v, Intent intent, Object tag) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // Only launch using the new animation if the shortcut has not opted out (this is a // private contract between launcher and may be ignored in the future). boolean useLaunchAnimation = (v != null) && @@ -2925,7 +3375,23 @@ public class Launcher extends Activity intent.getSourceBounds(), optsBundle); } return true; - } catch (SecurityException e) { + } catch (SecurityException | NullPointerException e) { + if (Utilities.ATLEAST_MARSHMALLOW && tag instanceof ItemInfo) { + // Due to legacy reasons, direct call shortcuts require Launchers to have the + // corresponding permission. Show the appropriate permission prompt if that + // is the case. + if (intent.getComponent() == null + && Intent.ACTION_CALL.equals(intent.getAction()) + && checkSelfPermission(Manifest.permission.CALL_PHONE) != + PackageManager.PERMISSION_GRANTED) { + // TODO: Rename sPendingAddItem to a generic name. + sPendingAddItem = preparePendingAddArgs(REQUEST_PERMISSION_CALL_PHONE, intent, + 0, (ItemInfo) tag); + requestPermissions(new String[]{Manifest.permission.CALL_PHONE}, + REQUEST_PERMISSION_CALL_PHONE); + return false; + } + } Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Log.e(TAG, "Launcher does not have the permission to launch " + intent + ". Make sure to create a MAIN intent-filter for the corresponding activity " + @@ -3061,10 +3527,15 @@ public class Launcher extends Activity * is animated relative to the specified View. If the View is null, no animation * is played. * - * @param folderInfo The FolderInfo describing the folder to open. + * @param folderIcon The FolderIcon describing the folder to open. */ - public void openFolder(FolderIcon folderIcon) { + public void openFolder(FolderIcon folderIcon, int[] folderTouch) { Folder folder = folderIcon.getFolder(); + + if (folder.getState() == Folder.STATE_ANIMATING) { + return; + } + Folder openFolder = mWorkspace != null ? mWorkspace.getOpenFolder() : null; if (openFolder != null && openFolder != folder) { // Close any open folder before opening a folder. @@ -3073,6 +3544,11 @@ public class Launcher extends Activity FolderInfo info = folder.mInfo; + if (info.hidden && !mHiddenFolderAuth) { + folder.startHiddenFolderManager(REQUEST_OPEN_PROTECTED_FOLDER); + return; + } + info.opened = true; // While the folder is open, the position of the icon cannot change. @@ -3086,9 +3562,10 @@ public class Launcher extends Activity } else { Log.w(TAG, "Opening folder (" + folder + ") which already has a parent (" + folder.getParent() + ")."); + return; } folder.animateOpen(); - growAndFadeOutFolderIcon(folderIcon); + /*growAndFadeOutFolderIcon(folderIcon);*/ // Notify the accessibility manager that this folder "window" has appeared and occluded // the workspace items @@ -3097,31 +3574,45 @@ public class Launcher extends Activity } public void closeFolder() { + closeFolder(true); + } + + public void closeFolder(boolean animate) { Folder folder = mWorkspace != null ? mWorkspace.getOpenFolder() : null; if (folder != null) { if (folder.isEditingName()) { folder.dismissEditingName(); } - closeFolder(folder); + closeFolder(folder, animate); } } public void closeFolder(Folder folder) { - folder.getInfo().opened = false; + closeFolder(folder, true); + } + + public void closeFolder(Folder folder, boolean animate) { + final FolderInfo info = folder.getInfo(); + info.opened = false; + if (info.hidden) { + mHiddenFolderAuth = false; + } ViewGroup parent = (ViewGroup) folder.getParent().getParent(); if (parent != null) { FolderIcon fi = (FolderIcon) mWorkspace.getViewForTag(folder.mInfo); - shrinkAndFadeInFolderIcon(fi); + /*shrinkAndFadeInFolderIcon(fi);*/ if (fi != null) { ((CellLayout.LayoutParams) fi.getLayoutParams()).canReorder = true; } } - folder.animateClosed(); + folder.animateClosed(mHiddenFolderLockStateChanged ? false : animate); // Notify the accessibility manager that this folder "window" has disappeard and no // longer occludeds the workspace items getDragLayer().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + mHiddenFolderIcon = null; + mHiddenFolderLockStateChanged = false; } public boolean onLongClick(View v) { @@ -3276,6 +3767,10 @@ public class Launcher extends Activity boolean changed = mState != State.WORKSPACE || mWorkspace.getState() != Workspace.State.NORMAL; if (changed) { + // Close overview mode if open + if (mWorkspace.isInOverviewMode()) { + mWorkspace.exitOverviewMode(); + } mWorkspace.setVisibility(View.VISIBLE); mStateTransitionAnimation.startAnimationToWorkspace(mState, mWorkspace.getState(), Workspace.State.NORMAL, snapToPage, animated, onCompleteRunnable); @@ -3288,6 +3783,8 @@ public class Launcher extends Activity // Change the state *after* we've called all the transition code mState = State.WORKSPACE; + setWorkspaceBackground(mState == State.WORKSPACE ? WORKSPACE_BACKGROUND_GRADIENT + : WORKSPACE_BACKGROUND_TRANSPARENT); // Resume the auto-advance of widgets mUserPresent = true; @@ -3321,6 +3818,8 @@ public class Launcher extends Activity tryAndUpdatePredictedApps(); } showAppsOrWidgets(State.APPS, animated, focusSearchBar); + + sRemoteFolderManager.onAppDrawerOpened(); } /** @@ -3440,10 +3939,15 @@ public class Launcher extends Activity * resumed. */ private void tryAndUpdatePredictedApps() { - if (mLauncherCallbacks != null) { - List<ComponentKey> apps = mLauncherCallbacks.getPredictedApps(); - if (apps != null) { - mAppsView.setPredictedApps(apps); + boolean mRemoteDrawerEnabled = SettingsProvider.getBoolean(this, + SettingsProvider.SETTINGS_UI_DRAWER_REMOTE_APPS, + R.bool.preferences_interface_drawer_remote_apps_default); + if (!mRemoteDrawerEnabled) { + if (mLauncherCallbacks != null) { + List<ComponentKey> apps = mLauncherCallbacks.getPredictedApps(); + if (apps != null) { + mAppsView.setPredictedAppComponents(apps); + } } } } @@ -3662,6 +4166,10 @@ public class Launcher extends Activity mWorkspace.createCustomContentContainer(); populateCustomContentContainer(); } + + LauncherModel.saveWidgetCount(this); + LauncherModel.savePageCount(this); + } @Override @@ -3730,6 +4238,7 @@ public class Launcher extends Activity if (addedApps != null && mAppsView != null) { mAppsView.addApps(addedApps); + sRemoteFolderManager.onBindAddApps(addedApps); } } @@ -3753,7 +4262,7 @@ public class Launcher extends Activity final AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); final Collection<Animator> bounceAnims = new ArrayList<Animator>(); final boolean animateIcons = forceAnimateIcons && canRunNewAppsAnimation(); - Workspace workspace = mWorkspace; + final Workspace workspace = mWorkspace; long newShortcutsScreenId = -1; for (int i = start; i < end; i++) { final ItemInfo item = shortcuts.get(i); @@ -3793,6 +4302,10 @@ public class Launcher extends Activity view = FolderIcon.fromXml(R.layout.folder_icon, this, (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), (FolderInfo) item, mIconCache); + if (view == null) { + continue; + } + ((FolderIcon) view).setTextVisible(!mHideIconLabels); break; default: throw new RuntimeException("Invalid Item Type"); @@ -4049,6 +4562,10 @@ public class Launcher extends Activity if (mLauncherCallbacks != null) { mLauncherCallbacks.finishBindingItems(false); } + + mWorkspace.stripEmptyScreens(); + + sRemoteFolderManager.bindFinished(); } private void sendLoadingCompleteBroadcastIfNecessary() { @@ -4248,7 +4765,7 @@ public class Launcher extends Activity for (AppInfo info : appInfos) { removedComponents.add(info.componentName); } - if (!packageNames.isEmpty()) { + if (packageNames != null && !packageNames.isEmpty()) { mWorkspace.removeItemsByPackageName(packageNames, user); } if (!removedComponents.isEmpty()) { @@ -4719,6 +5236,64 @@ public class Launcher extends Activity }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); } } + + private void setupSearchBar(Context context) { + mIsDrawerSearchBarEnabled = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_DRAWER_SEARCH, + R.bool.preferences_interface_homescreen_search_default); + + if (mIsDrawerSearchBarEnabled) { + if (mLauncherCallbacks != null + && mLauncherCallbacks.getAllAppsSearchBarController() != null) { + mAppsView + .setSearchBarController(mLauncherCallbacks.getAllAppsSearchBarController()); + } else { + mAppsView.setSearchBarController(mAppsView.newDefaultAppSearchController()); + } + } else { + mAppsView.setSearchBarController(null); + mAppsView.setHasSearchBar(mIsDrawerSearchBarEnabled); + mAppsView.setSearchBarContainerViewVisibility( + mIsDrawerSearchBarEnabled ? View.VISIBLE : View.GONE); + mAppsView.updateBackgroundAndPaddings(); + } + } + + class SettingsPanelSlideListener extends VerticalSlidingPanel.SimplePanelSlideListener { + ImageView mAnimatedArrow; + + public SettingsPanelSlideListener() { + super(); + mAnimatedArrow = (ImageView) mOverviewPanel.findViewById(R.id.settings_drag_arrow); + } + + @Override + public void onPanelCollapsed(View panel) { + mAnimatedArrow.setBackgroundResource(R.drawable.transition_arrow_reverse); + + AnimationDrawable frameAnimation = (AnimationDrawable) mAnimatedArrow.getBackground(); + frameAnimation.start(); + } + + @Override + public void onPanelExpanded(View panel) { + mAnimatedArrow.setBackgroundResource(R.drawable.transition_arrow); + + AnimationDrawable frameAnimation = (AnimationDrawable) mAnimatedArrow.getBackground(); + frameAnimation.start(); + + LauncherApplication.getLauncherStats().sendSettingsOpenedEvent( + LauncherStats.ORIGIN_TREB_LONGPRESS); + } + + @Override + public void onPanelShown(View panel) { + mAnimatedArrow.setBackgroundResource(R.drawable.transition_arrow_reverse); + + AnimationDrawable frameAnimation = (AnimationDrawable) mAnimatedArrow.getBackground(); + frameAnimation.start(); + } + } } interface DebugIntents { diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index d87ad67e5..d2a9b2641 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -124,6 +124,10 @@ public class LauncherAppState { mModel.startLoaderFromBackground(); } + public void recreateWidgetPreviewDb() { + mWidgetCache.recreateWidgetPreviewDb(); + } + LauncherModel setLauncher(Launcher launcher) { getLauncherProvider().setLauncherProviderChangeListener(launcher); mModel.initialize(launcher); @@ -174,6 +178,10 @@ public class LauncherAppState { return mInvariantDeviceProfile; } + public void initInvariantDeviceProfile() { + mInvariantDeviceProfile = new InvariantDeviceProfile(sContext); + } + public static boolean isDogfoodBuild() { return getInstance().mBuildInfo.isDogfoodBuild(); } diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherApplication.java new file mode 100644 index 000000000..4bbcec073 --- /dev/null +++ b/src/com/android/launcher3/LauncherApplication.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2013 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; + +import android.app.Application; + +import com.android.launcher3.stats.LauncherStats; +import com.android.launcher3.stats.internal.service.AggregationIntentService; + +public class LauncherApplication extends Application { + + private static LauncherStats sLauncherStats = null; + + /** + * Get the reference handle for LauncherStats commands + * + * @return {@link LauncherStats} + */ + public static LauncherStats getLauncherStats() { + return sLauncherStats; + } + + @Override + public void onCreate() { + super.onCreate(); + sLauncherStats = LauncherStats.getInstance(this); + AggregationIntentService.scheduleService(this); + } + +} diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index b5922c6a3..e90ea1765 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.Intent; import android.content.Intent.ShortcutIconResource; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; @@ -62,6 +63,10 @@ import com.android.launcher3.util.CursorIconInfo; import com.android.launcher3.util.LongArrayMap; import com.android.launcher3.util.ManagedProfileHeuristic; import com.android.launcher3.util.Thunk; +import cyanogenmod.providers.CMSettings; + +import com.android.launcher3.settings.SettingsProvider; +import com.android.launcher3.stats.internal.service.AggregationIntentService; import java.lang.ref.WeakReference; import java.net.URISyntaxException; @@ -94,6 +99,7 @@ public class LauncherModel extends BroadcastReceiver public static final int LOADER_FLAG_NONE = 0; public static final int LOADER_FLAG_CLEAR_WORKSPACE = 1 << 0; public static final int LOADER_FLAG_MIGRATE_SHORTCUTS = 1 << 1; + public static final int LOADER_FLAG_RESIZE_GRID = 1 << 2; private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons private static final long INVALID_SCREEN_ID = -1L; @@ -108,6 +114,8 @@ public class LauncherModel extends BroadcastReceiver @Thunk boolean mIsLoaderTaskRunning; @Thunk boolean mHasLoaderCompletedOnce; + private volatile boolean mFlushingWorkerThread; + private static final String MIGRATE_AUTHORITY = "com.android.launcher2.settings"; @Thunk static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); @@ -433,6 +441,33 @@ public class LauncherModel extends BroadcastReceiver ArrayList<Long> workspaceScreens, ArrayList<Long> addedWorkspaceScreensFinal, int spanX, int spanY) { + + // Preferred screen is the next one after the default. + long preferredScreenId = SettingsProvider.getLongCustomDefault(context, + SettingsProvider.SETTINGS_UI_HOMESCREEN_DEFAULT_SCREEN_ID, + R.integer.preferences_interface_homescreen_id_default); + int preferredScreenIndex = 0; + for (int i = 0; i < workspaceScreens.size(); i++) { + if (workspaceScreens.get(i) == preferredScreenId) { + preferredScreenIndex = i + 1; + break; + } + } + + return findSpaceForItem(context, workspaceScreens, addedWorkspaceScreensFinal, + spanX, spanY, preferredScreenIndex); + } + + /** + * Find a position on the screen for the given size or adds a new screen. Checks + * preferredScreen first, and if no space is found then starts searching from the left. + * @return screenId and the coordinates for the item. + */ + @Thunk Pair<Long, int[]> findSpaceForItem( + Context context, + ArrayList<Long> workspaceScreens, + ArrayList<Long> addedWorkspaceScreensFinal, + int spanX, int spanY, int preferredScreenIndex) { LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>(); // Use sBgItemsIdMap as all the items are already loaded. @@ -450,6 +485,12 @@ public class LauncherModel extends BroadcastReceiver } } + // If we have a zero-id screen then we skip over it. + boolean hasZero = false; + if (!workspaceScreens.isEmpty() && workspaceScreens.get(0) == 0) { + hasZero = true; + } + // Find appropriate space for the item. long screenId = 0; int[] cordinates = new int[2]; @@ -457,7 +498,6 @@ public class LauncherModel extends BroadcastReceiver int screenCount = workspaceScreens.size(); // First check the preferred screen. - int preferredScreenIndex = workspaceScreens.isEmpty() ? 0 : 1; if (preferredScreenIndex < screenCount) { screenId = workspaceScreens.get(preferredScreenIndex); found = findNextAvailableIconSpaceInScreen( @@ -466,7 +506,8 @@ public class LauncherModel extends BroadcastReceiver if (!found) { // Search on any of the screens starting from the first screen. - for (int screen = 1; screen < screenCount; screen++) { + int firstScreen = hasZero ? 1 : 0; + for (int screen = firstScreen; screen < screenCount; screen++) { screenId = workspaceScreens.get(screen); if (findNextAvailableIconSpaceInScreen( screenItems.get(screenId), cordinates, spanX, spanY)) { @@ -650,27 +691,27 @@ public class LauncherModel extends BroadcastReceiver modelShortcut.spanX == shortcut.spanX && modelShortcut.spanY == shortcut.spanY && ((modelShortcut.dropPos == null && shortcut.dropPos == null) || - (modelShortcut.dropPos != null && - shortcut.dropPos != null && - modelShortcut.dropPos[0] == shortcut.dropPos[0] && - modelShortcut.dropPos[1] == shortcut.dropPos[1]))) { + (modelShortcut.dropPos != null && + shortcut.dropPos != null && + modelShortcut.dropPos[0] == shortcut.dropPos[0] && + modelShortcut.dropPos[1] == shortcut.dropPos[1]))) { // For all intents and purposes, this is the same object return; } - } - // the modelItem needs to match up perfectly with item if our model is - // to be consistent with the database-- for now, just require - // modelItem == item or the equality check above - String msg = "item: " + ((item != null) ? item.toString() : "null") + - "modelItem: " + - ((modelItem != null) ? modelItem.toString() : "null") + - "Error: ItemInfo passed to checkItemInfo doesn't match original"; - RuntimeException e = new RuntimeException(msg); - if (stackTrace != null) { - e.setStackTrace(stackTrace); + // the modelItem needs to match up perfectly with item if our model is + // to be consistent with the database-- for now, just require + // modelItem == item or the equality check above + String msg = "item: " + ((item != null) ? item.toString() : "null") + + "modelItem: " + + ((modelItem != null) ? modelItem.toString() : "null") + + "Error: ItemInfo passed to checkItemInfo doesn't match original"; + RuntimeException e = new RuntimeException(msg); + if (stackTrace != null) { + e.setStackTrace(stackTrace); + } + throw e; } - throw e; } } @@ -774,6 +815,35 @@ public class LauncherModel extends BroadcastReceiver } } + public void flushWorkerThread() { + mFlushingWorkerThread = true; + Runnable waiter = new Runnable() { + public void run() { + synchronized (this) { + notifyAll(); + mFlushingWorkerThread = false; + } + } + }; + + synchronized(waiter) { + runOnWorkerThread(waiter); + if (mLoaderTask != null) { + synchronized(mLoaderTask) { + mLoaderTask.notify(); + } + } + boolean success = false; + while (!success) { + try { + waiter.wait(); + success = true; + } catch (InterruptedException e) { + } + } + } + } + /** * Move an item in the DB to a new <container, screen, cellX, cellY> */ @@ -950,6 +1020,8 @@ public class LauncherModel extends BroadcastReceiver final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); final int optionsIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.OPTIONS); + final int hiddenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.HIDDEN); + final int subType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SUBTYPE); FolderInfo folderInfo = null; switch (c.getInt(itemTypeIndex)) { @@ -966,6 +1038,8 @@ public class LauncherModel extends BroadcastReceiver folderInfo.cellX = c.getInt(cellXIndex); folderInfo.cellY = c.getInt(cellYIndex); folderInfo.options = c.getInt(optionsIndex); + folderInfo.hidden = c.getInt(hiddenIndex) > 0; + folderInfo.subType = subType; return folderInfo; } @@ -977,10 +1051,22 @@ public class LauncherModel extends BroadcastReceiver } /** + * Saves the total widget count to a shared preference + * + * @param context {@link Context} + */ + /* package */ static void saveWidgetCount(Context context) { + int widgetCount = LauncherModel.sBgAppWidgets.size(); + SharedPreferences prefs = context.getSharedPreferences(LauncherAppState + .getSharedPreferencesKey(), Context.MODE_PRIVATE); + prefs.edit().putInt(AggregationIntentService.PREF_KEY_WIDGET_COUNT, widgetCount).apply(); + } + + /** * Add an item to the database in a specified container. Sets the container, screen, cellX and * cellY fields of the item. Also assigns an ID to the item. */ - public static void addItemToDatabase(Context context, final ItemInfo item, final long container, + public static void addItemToDatabase(final Context context, final ItemInfo item, final long container, final long screenId, final int cellX, final int cellY) { item.container = container; item.cellX = cellX; @@ -1030,6 +1116,7 @@ public class LauncherModel extends BroadcastReceiver break; case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: sBgAppWidgets.add((LauncherAppWidgetInfo) item); + saveWidgetCount(context); break; } } @@ -1080,9 +1167,9 @@ public class LauncherModel extends BroadcastReceiver /** * Removes the specified items from the database * @param context - * @param item + * @param items */ - static void deleteItemsFromDatabase(Context context, final ArrayList<? extends ItemInfo> items) { + static void deleteItemsFromDatabase(final Context context, final ArrayList<? extends ItemInfo> items) { final ContentResolver cr = context.getContentResolver(); Runnable r = new Runnable() { public void run() { @@ -1112,6 +1199,7 @@ public class LauncherModel extends BroadcastReceiver break; case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: sBgAppWidgets.remove((LauncherAppWidgetInfo) item); + saveWidgetCount(context); break; } sBgItemsIdMap.remove(item.id); @@ -1123,10 +1211,22 @@ public class LauncherModel extends BroadcastReceiver } /** + * Saves the count of workspace pages + * + * @param context {@link Context} + */ + /* package */ static void savePageCount(Context context) { + int pageCount = LauncherModel.sBgWorkspaceScreens.size(); + SharedPreferences prefs = context.getSharedPreferences(LauncherAppState + .getSharedPreferencesKey(), Context.MODE_PRIVATE); + prefs.edit().putInt(AggregationIntentService.PREF_KEY_PAGE_COUNT, pageCount).apply(); + } + + /** * Update the order of the workspace screens in the database. The array list contains * a list of screen ids in the order that they should appear. */ - public void updateWorkspaceScreenOrder(Context context, final ArrayList<Long> screens) { + public void updateWorkspaceScreenOrder(final Context context, final ArrayList<Long> screens) { final ArrayList<Long> screensCopy = new ArrayList<Long>(screens); final ContentResolver cr = context.getContentResolver(); final Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI; @@ -1164,6 +1264,7 @@ public class LauncherModel extends BroadcastReceiver synchronized (sBgLock) { sBgWorkspaceScreens.clear(); sBgWorkspaceScreens.addAll(screensCopy); + savePageCount(context); } } }; @@ -1612,7 +1713,7 @@ public class LauncherModel extends BroadcastReceiver // check & update map of what's occupied; used to discard overlapping/invalid items private boolean checkItemPlacement(LongArrayMap<ItemInfo[][]> occupied, ItemInfo item, - ArrayList<Long> workspaceScreens) { + ArrayList<Long> workspaceScreens, boolean shouldResizeAndUpdateDB) { LauncherAppState app = LauncherAppState.getInstance(); InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); final int countX = profile.numColumns; @@ -1668,26 +1769,67 @@ public class LauncherModel extends BroadcastReceiver return true; } - if (!occupied.containsKey(item.screenId)) { - ItemInfo[][] items = new ItemInfo[countX + 1][countY + 1]; - occupied.put(item.screenId, items); + // If the current item's position lies outside of the bounds + // of the current grid size, attempt to place it in the next + // available position. + if (item.cellX < 0 || item.cellY < 0 || item.cellX + item.spanX > countX + || item.cellY + item.spanY > countY) { + // If we won't be resizing the grid, then just return, this item does not fit. + if (!shouldResizeAndUpdateDB) { + Log.e(TAG, "Error loading shortcut " + item + + " into cell (" + containerIndex + "-" + item.screenId + ":" + + item.cellX + "," + item.cellY + + ") out of screen bounds ( " + countX + "x" + countY + ")"); + return false; + } + + if (item.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) { + // Place the item at 0 0 of screen 1 + // if items overlap here, they will be moved later on + item.cellX = 0; + item.cellY = 0; + item.screenId = 1; + item.wasMovedDueToReducedSpace = true; + item.requiresDbUpdate = true; + } else { + // see if widget can be shrunk to fit a screen, if not, just remove it + if (item.minSpanX > countX || item.minSpanY > countY) { + return false; + } + // if the widget is larger than the grid, shrink it down + if (item.cellX + item.spanX > countX) { + item.cellX = 0; + item.spanY = (item.spanY / 2) > 0 ? item.spanY / 2 : 1; + item.spanX = item.minSpanX; + item.requiresDbUpdate = true; + item.wasMovedDueToReducedSpace = true; + } + if (item.cellY + item.spanY > countY) { + item.cellY = 0; + item.spanY = countY; + item.requiresDbUpdate = true; + item.wasMovedDueToReducedSpace = true; + } + if (item.cellY + item.spanY == countY && item.cellX + item.spanX == countX) { + // if the widget is the size of the grid, make a screen all it's own. + item.screenId = sBgWorkspaceScreens.size() + 1; + } + } + } else { + item.wasMovedDueToReducedSpace = false; + item.requiresDbUpdate = true; } - final ItemInfo[][] screens = occupied.get(item.screenId); - if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && - item.cellX < 0 || item.cellY < 0 || - item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) { - Log.e(TAG, "Error loading shortcut " + item - + " into cell (" + containerIndex + "-" + item.screenId + ":" - + item.cellX + "," + item.cellY - + ") out of screen bounds ( " + countX + "x" + countY + ")"); - return false; + if (!occupied.containsKey(item.screenId)) { + ItemInfo[][] items = new ItemInfo[countX][countY]; + occupied.put(item.screenId, items); } + ItemInfo[][] screens = occupied.get(item.screenId); // Check if any workspace icons overlap with each other for (int x = item.cellX; x < (item.cellX+item.spanX); x++) { for (int y = item.cellY; y < (item.cellY+item.spanY); y++) { - if (screens[x][y] != null) { + if (!shouldResizeAndUpdateDB && screens[x][y] != null) { Log.e(TAG, "Error loading shortcut " + item + " into cell (" + containerIndex + "-" + item.screenId + ":" + x + "," + y @@ -1700,6 +1842,16 @@ public class LauncherModel extends BroadcastReceiver for (int x = item.cellX; x < (item.cellX+item.spanX); x++) { for (int y = item.cellY; y < (item.cellY+item.spanY); y++) { screens[x][y] = item; + if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET + && shouldResizeAndUpdateDB) { + // fill up the entire grid where the widget technically is + for (int spanX = x; spanX < item.spanX; spanX++) { + screens[spanX][y] = item; + for (int spanY = y; spanY < item.spanX; spanY++) { + screens[spanX][spanY] = item; + } + } + } } } @@ -1733,6 +1885,8 @@ public class LauncherModel extends BroadcastReceiver int countX = profile.numColumns; int countY = profile.numRows; + boolean shouldResize = ((mFlags & LOADER_FLAG_RESIZE_GRID) != 0); + if (MigrateFromRestoreTask.ENABLED && MigrateFromRestoreTask.shouldRunTask(mContext)) { long migrationStartTime = System.currentTimeMillis(); Log.v(TAG, "Starting workspace migration after restore"); @@ -1816,6 +1970,9 @@ public class LauncherModel extends BroadcastReceiver LauncherSettings.Favorites.PROFILE_ID); final int optionsIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.OPTIONS); + final int hiddenIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.HIDDEN); + final int subTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SUBTYPE); final CursorIconInfo cursorIconInfo = new CursorIconInfo(c); final LongSparseArray<UserHandleCompat> allUsers = new LongSparseArray<>(); @@ -2044,7 +2201,8 @@ public class LauncherModel extends BroadcastReceiver } // check & update map of what's occupied - if (!checkItemPlacement(occupied, info, sBgWorkspaceScreens)) { + if (!checkItemPlacement(occupied, info, sBgWorkspaceScreens, + shouldResize)) { itemsToRemove.add(id); break; } @@ -2093,9 +2251,12 @@ public class LauncherModel extends BroadcastReceiver folderInfo.spanX = 1; folderInfo.spanY = 1; folderInfo.options = c.getInt(optionsIndex); + folderInfo.hidden = c.getInt(hiddenIndex) > 0; + folderInfo.subType = c.getInt(subTypeIndex); // check & update map of what's occupied - if (!checkItemPlacement(occupied, folderInfo, sBgWorkspaceScreens)) { + if (!checkItemPlacement(occupied, folderInfo, sBgWorkspaceScreens, + shouldResize)) { itemsToRemove.add(id); break; } @@ -2222,7 +2383,8 @@ public class LauncherModel extends BroadcastReceiver appWidgetInfo.container = container; // check & update map of what's occupied - if (!checkItemPlacement(occupied, appWidgetInfo, sBgWorkspaceScreens)) { + if (!checkItemPlacement(occupied, appWidgetInfo, + sBgWorkspaceScreens, shouldResize)) { itemsToRemove.add(id); break; } @@ -2327,6 +2489,17 @@ public class LauncherModel extends BroadcastReceiver updateWorkspaceScreenOrder(context, sBgWorkspaceScreens); } + // If any items have been shifted and require a DB update, update them in the DB. + if (shouldResize) { + for (ItemInfo info : sBgWorkspaceItems) { + if (info != null && info.requiresDbUpdate) { + info.requiresDbUpdate = false; + LauncherModel.modifyItemInDatabase(mContext, info, info.container, + info.screenId, info.cellX, info.cellY, info.spanX, info.spanY); + } + } + } + if (DEBUG_LOADERS) { Log.d(TAG, "loaded workspace in " + (SystemClock.uptimeMillis()-t) + "ms"); Log.d(TAG, "workspace layout: "); @@ -2488,6 +2661,111 @@ public class LauncherModel extends BroadcastReceiver runOnMainThread(r); } + private void removeHiddenAppsWorkspaceItems( + final ArrayList<ItemInfo> workspaceItems, + final ArrayList<LauncherAppWidgetInfo> appWidgets, + final LongArrayMap<FolderInfo> folders) { + + // Get hidden apps + ArrayList<ComponentName> mHiddenApps = new ArrayList<ComponentName>(); + ArrayList<String> mHiddenAppsPackages = new ArrayList<String>(); + Context context = mApp.getContext(); + String protectedComponents = CMSettings.Secure.getString(context.getContentResolver(), + CMSettings.Secure.PROTECTED_COMPONENTS); + protectedComponents = protectedComponents == null ? "" : protectedComponents; + String[] flattened = protectedComponents.split("\\|"); + + for (String flat : flattened) { + ComponentName cmp = ComponentName.unflattenFromString(flat); + if (cmp != null) { + mHiddenApps.add(cmp); + mHiddenAppsPackages.add(cmp.getPackageName()); + } + } + + // Shortcuts + int N = workspaceItems.size() - 1; + for (int i = N; i >= 0; i--) { + final ItemInfo item = workspaceItems.get(i); + if (item instanceof ShortcutInfo) { + ShortcutInfo shortcut = (ShortcutInfo)item; + if (shortcut.intent != null && shortcut.intent.getComponent() != null) { + if (mHiddenApps.contains(shortcut.intent.getComponent())) { + LauncherModel.deleteItemFromDatabase(mContext, shortcut); + workspaceItems.remove(i); + } + } + } else { + // Only remove items from folders that aren't hidden + final FolderInfo folder = (FolderInfo)item; + List<ShortcutInfo> shortcuts = folder.contents; + + int NN = shortcuts.size() - 1; + for (int j = NN; j >= 0; j--) { + final ShortcutInfo sci = shortcuts.get(j); + if (sci.intent != null && sci.intent.getComponent() != null) { + if (!folder.hidden){ + if (mHiddenApps.contains(sci.intent.getComponent())) { + LauncherModel.deleteItemFromDatabase(mContext, sci); + Runnable r = new Runnable() { + public void run() { + folder.remove(sci); + } + }; + runOnMainThread(r); + } + } else { + if (!mHiddenApps.contains(sci.intent.getComponent())) { + LauncherModel.deleteItemFromDatabase(mContext, sci); + Runnable r = new Runnable() { + public void run() { + folder.remove(sci); + } + }; + runOnMainThread(r); + } + } + + } + } + + if (folder.contents.size() == 1 && !folder.hidden) { + ShortcutInfo finalItem = folder.contents.get(0); + finalItem.container = folder.container; + LauncherModel.deleteItemFromDatabase(mContext, folder); + // only replace this item back on the workspace if it's not protected + // and not a remote folder. + if (!mHiddenApps.contains(finalItem.intent.getComponent()) && + !folder.isRemote()) { + LauncherModel.addOrMoveItemInDatabase(mContext, finalItem, + folder.container, folder.screenId, folder.cellX, folder.cellY); + workspaceItems.add(finalItem); + } + workspaceItems.remove(i); + folders.remove(Long.valueOf(item.id)); + + // Remote folders are always empty on bind. + } else if (folder.contents.size() == 0 && !folder.isRemote()) { + LauncherModel.deleteFolderContentsFromDatabase(mContext, folder); + workspaceItems.remove(i); + folders.remove(Long.valueOf(item.id)); + } + } + } + + // AppWidgets + N = appWidgets.size() - 1; + for (int i = N; i >= 0; i--) { + final LauncherAppWidgetInfo item = appWidgets.get(i); + if (item.providerName != null) { + if (mHiddenAppsPackages.contains(item.providerName.getPackageName())) { + LauncherModel.deleteItemFromDatabase(mContext, item); + appWidgets.remove(i); + } + } + } + } + private void bindWorkspaceItems(final Callbacks oldCallbacks, final ArrayList<ItemInfo> workspaceItems, final ArrayList<LauncherAppWidgetInfo> appWidgets, @@ -2496,6 +2774,8 @@ public class LauncherModel extends BroadcastReceiver final boolean postOnMainThread = (deferredBindRunnables != null); + removeHiddenAppsWorkspaceItems(workspaceItems, appWidgets, folders); + // Bind the workspace items int N = workspaceItems.size(); for (int i = 0; i < N; i += ITEMS_CHUNK) { @@ -3305,9 +3585,12 @@ public class LauncherModel extends BroadcastReceiver // Refresh widget list, if there is any newly added widget PackageManager pm = context.getPackageManager(); for (String pkg : mPackages) { - needToRefresh |= !pm.queryBroadcastReceivers( + List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers( new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE) - .setPackage(pkg), 0).isEmpty(); + .setPackage(pkg), 0); + if (resolveInfos != null) { + needToRefresh |= !resolveInfos.isEmpty(); + } } } diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 8791e9e57..022ccaccd 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -63,12 +63,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Set; public class LauncherProvider extends ContentProvider { private static final String TAG = "LauncherProvider"; private static final boolean LOGD = false; - private static final int DATABASE_VERSION = 26; + private static final int DATABASE_VERSION = 28; public static final String AUTHORITY = ProviderConfig.AUTHORITY; @@ -537,8 +538,10 @@ public class LauncherProvider extends ContentProvider { "modified INTEGER NOT NULL DEFAULT 0," + "restored INTEGER NOT NULL DEFAULT 0," + "profileId INTEGER DEFAULT " + userSerialNumber + "," + + "hidden INTEGER DEFAULT 0" + "," + "rank INTEGER NOT NULL DEFAULT 0," + - "options INTEGER NOT NULL DEFAULT 0" + + "options INTEGER NOT NULL DEFAULT 0," + + "subType INTEGER DEFAULT 0" + ");"); addWorkspacesTable(db); @@ -568,7 +571,8 @@ public class LauncherProvider extends ContentProvider { setFlagEmptyDbCreated(); // When a new DB is created, remove all previously stored managed profile information. - ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(), mContext); + ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(), + mContext); } private void addWorkspacesTable(SQLiteDatabase db) { @@ -673,6 +677,42 @@ public class LauncherProvider extends ContentProvider { } } case 16: { + + Log.w(TAG, "Found pre-11 Trebuchet, preparing update"); + + // With the new shrink-wrapped and re-orderable workspaces, it makes sense + // to persist workspace screens and their relative order. + mMaxScreenId = 0; + + addWorkspacesTable(db); + + Cursor c = null; + long screenId = getMaxId(db, TABLE_FAVORITES); + + + db.beginTransaction(); + try { + // Insert new column for holding widget provider name + db.execSQL("ALTER TABLE favorites " + + "ADD COLUMN appWidgetProvider TEXT;"); + addIntegerColumn(db, "modified", 0); + // Create workspaces for the migrated things + if (screenId > 0) { + for (int sId = 0; sId <= screenId; sId++) { + db.execSQL("INSERT INTO workspaceScreens (_id, screenRank) " + + "VALUES (" + (sId+1) + ", " + sId + ")"); + } + } + // Adjust hotseat format + db.execSQL("UPDATE favorites SET screen=cellX WHERE container=-101;"); + db.setTransactionSuccessful(); + } catch (SQLException ex) { + // Old version remains, which means we wipe old data + Log.e(TAG, ex.getMessage(), ex); + } finally { + db.endTransaction(); + } + // We use the db version upgrade here to identify users who may not have seen // clings yet (because they weren't available), but for whom the clings are now // available (tablet users). Because one of the possible cling flows (migration) @@ -711,6 +751,10 @@ public class LauncherProvider extends ContentProvider { // Old version remains, which means we wipe old data break; } + if (!ensureRankColumn(db) || !updateFolderItemsRank(db, false)) { + // Old version remains, which means we wipe old data + break; + } } case 23: // No-op @@ -718,9 +762,38 @@ public class LauncherProvider extends ContentProvider { ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mContext); case 25: convertShortcutsToLauncherActivities(db); - case 26: { - // DB Upgraded successfully - return; + case 26: + if (!ensureHiddenColumn(db)) { + // Old version remains, which means we wipe old data + break; + } + case 27: { + migrateLauncherFavorite(db, "com.android.dialer", "com.cyngn.dialer", + "com.android.dialer.DialtactsActivity", + "com.android.dialer.DialtactsActivity"); + migrateLauncherFavorite(db, "com.android.mms", "com.android.messaging", + "com.android.mms.ui.ConversationList", + "com.android.messaging.ui.conversationlist.ConversationListActivity"); + migrateLauncherFavorite(db, "com.android.camera2", "org.cyanogenmod.snap", + "com.android.camera.CameraLauncher", + "com.android.camera.CameraLauncher"); + migrateLauncherFavorite(db, "org.cyanogenmod.snap", "com.android.camera2", + "com.android.camera.CameraLauncher", + "com.android.camera.CameraLauncher"); + } + case 28: { + db.beginTransaction(); + try { + db.execSQL("ALTER TABLE favorites " + + "ADD COLUMN subType INTEGER DEFAULT 0;"); + db.setTransactionSuccessful(); + db.endTransaction(); + return; + } catch (SQLException ex) { + // Old version remains, which means we wipe old data + Log.e(TAG, ex.getMessage(), ex); + db.endTransaction(); + } } } @@ -863,12 +936,12 @@ public class LauncherProvider extends ContentProvider { Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites" + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)" + " GROUP BY container;", - new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}); + new String[]{Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}); while (c.moveToNext()) { db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE " - + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;", - new Object[] {c.getLong(1) + 1, c.getLong(0)}); + + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;", + new Object[]{c.getLong(1) + 1, c.getLong(0)}); } c.close(); @@ -883,6 +956,36 @@ public class LauncherProvider extends ContentProvider { return true; } + @Thunk boolean ensureRankColumn(SQLiteDatabase db) { + try { + // Make sure rank exists + Cursor c = db.rawQuery("SELECT rank FROM favorites;", null); + if (c != null) { + c.close(); + } + } catch (SQLException ex) { + // Old version remains, which means we wipe old data + Log.e(TAG, ex.getMessage(), ex); + return addIntegerColumn(db, Favorites.RANK, 0); + } + return true; + } + + @Thunk boolean ensureHiddenColumn(SQLiteDatabase db) { + try { + // Make sure hidden exists + Cursor c = db.rawQuery("SELECT hidden FROM favorites;", null); + if (c != null) { + c.close(); + } + } catch (SQLException ex) { + // Old version remains, which means we wipe old data + Log.e(TAG, ex.getMessage(), ex); + return addIntegerColumn(db, Favorites.HIDDEN, 0); + } + return true; + } + private boolean addProfileColumn(SQLiteDatabase db) { UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); // Default to the serial number of this user, for older @@ -892,6 +995,71 @@ public class LauncherProvider extends ContentProvider { return addIntegerColumn(db, Favorites.PROFILE_ID, userSerialNumber); } + private void migrateLauncherFavorite(SQLiteDatabase db, String originalPackageName, + String newPackageName, String originalClassName, String newClassName) { + if (!Utilities.isPackageInstalled(mContext, newPackageName)) { + return; + } + + final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, + new int[]{Favorites.ITEM_TYPE_SHORTCUT, Favorites.ITEM_TYPE_APPLICATION}); + Cursor c = null; + db.beginTransaction(); + + try { + // Select and iterate through each matching widget + c = db.query(TABLE_FAVORITES, + new String[] { Favorites._ID, Favorites.INTENT }, + selectWhere, null, null, null, null); + if (c == null) return; + + while (c.moveToNext()) { + long favoriteId = c.getLong(0); + final String intentUri = c.getString(1); + if (intentUri != null) { + try { + final Intent intent = Intent.parseUri(intentUri, 0); + final ComponentName componentName = intent.getComponent(); + final Set<String> categories = intent.getCategories(); + + if (Intent.ACTION_MAIN.equals(intent.getAction()) && + componentName != null && + originalPackageName.equals(componentName.getPackageName()) && + originalClassName.equals(componentName.getClassName()) && + categories != null && + categories.contains(Intent.CATEGORY_LAUNCHER)) { + + final ComponentName newName = new ComponentName(newPackageName, + newClassName); + intent.setComponent(newName); + final ContentValues values = new ContentValues(); + values.put(Favorites.INTENT, intent.toUri(0)); + + String updateWhere = Favorites._ID + "=" + favoriteId; + db.update(TABLE_FAVORITES, values, updateWhere, null); + if (LOGD) { + Log.i(TAG, "Updated " + componentName + " to " + newName); + } + } + } catch (RuntimeException ex) { + Log.e(TAG, "Problem moving " + originalClassName + " activity", ex); + } catch (URISyntaxException e) { + Log.e(TAG, "Problem moving " + originalClassName + " activity", e); + } + } + } + + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.w(TAG, "Problem while upgrading " + originalClassName + " icon", ex); + } finally { + db.endTransaction(); + if (c != null) { + c.close(); + } + } + } + private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) { db.beginTransaction(); try { @@ -1391,6 +1559,21 @@ public class LauncherProvider extends ContentProvider { return id; } + /** + * Build a query string that will match any row where the column matches + * anything in the values list. + */ + private static String buildOrWhereString(String column, int[] values) { + StringBuilder selectWhere = new StringBuilder(); + for (int i = values.length - 1; i >= 0; i--) { + selectWhere.append(column).append("=").append(values[i]); + if (i > 0) { + selectWhere.append(" OR "); + } + } + return selectWhere.toString(); + } + static class SqlArguments { public final String table; public final String where; diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 8a5804f34..20428670b 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -42,6 +42,15 @@ public class LauncherSettings { public static final String TITLE = "title"; /** + * Folder Hidden status + */ + public static final String HIDDEN = "hidden"; + + /** + * Folder subtype, used for Remote Folders + */ + static final String SUBTYPE = "subType"; + /** * The Intent URL of the gesture, describing what it points to. This * value is given to {@link android.content.Intent#parseUri(String, int)} to create * an Intent that can be launched. @@ -309,6 +318,12 @@ public class LauncherSettings { * <p>Type: INTEGER</p> */ public static final String OPTIONS = "options"; + + /** + * Stores whether the item is hidden. + * <p>Type: INTEGER</p> + */ + public static final String HIDDEN = "hidden"; } /** diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java index cdde8c13f..2bdee3790 100644 --- a/src/com/android/launcher3/LauncherStateTransitionAnimation.java +++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java @@ -24,6 +24,7 @@ import android.animation.PropertyValuesHolder; import android.animation.TimeInterpolator; import android.annotation.SuppressLint; import android.content.res.Resources; +import android.graphics.drawable.Drawable; import android.util.Log; import android.view.View; import android.view.animation.AccelerateInterpolator; @@ -168,6 +169,11 @@ public class LauncherStateTransitionAnimation { public float getMaterialRevealViewFinalAlpha(View revealView) { return 0.3f; } + + @Override + void onTransitionComplete() { + toView.setScrubberVisibility(View.VISIBLE); + } }; mCurrentAnimation = startAnimationToOverlay(fromWorkspaceState, Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, toView.getContentView(), @@ -282,6 +288,14 @@ public class LauncherStateTransitionAnimation { // Setup the animation for the content view contentView.setVisibility(View.VISIBLE); + /* + * Because the contentView and revealView overlap, if their backgrounds have alpha, + * it will blend and cause a flicker. So get the current alpha of the contentView (for + * later) and set to 0. When the animation is done, reset the alpha. + */ + final Drawable contentBackground = contentView.getBackground(); + final int alpha = contentBackground.getAlpha(); + contentBackground.setAlpha(0); contentView.setAlpha(0f); contentView.setTranslationY(revealViewToYDrift); layerViews.put(contentView, BUILD_AND_SET_LAYER); @@ -322,6 +336,7 @@ public class LauncherStateTransitionAnimation { // Hide the reveal view revealView.setVisibility(View.INVISIBLE); + contentBackground.setAlpha(alpha); // Disable all necessary layers for (View v : layerViews.keySet()) { @@ -453,6 +468,7 @@ public class LauncherStateTransitionAnimation { final Workspace.State toWorkspaceState, final int toWorkspacePage, final boolean animated, final Runnable onCompleteRunnable) { final WidgetsContainerView widgetsView = mLauncher.getWidgetsView(); + widgetsView.setScrubberVisibility(View.INVISIBLE); PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { @Override float getMaterialRevealViewFinalAlpha(View revealView) { @@ -486,6 +502,9 @@ public class LauncherStateTransitionAnimation { final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); final Resources res = mLauncher.getResources(); final boolean material = Utilities.ATLEAST_LOLLIPOP; + final boolean doReveal = !((mLauncher.mState == Launcher.State.APPS || + mLauncher.mState == Launcher.State.WIDGETS) && + toWorkspaceState == Workspace.State.OVERVIEW); final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime); final int itemsAlphaStagger = res.getInteger(R.integer.config_overlayItemsAlphaStagger); @@ -515,13 +534,17 @@ public class LauncherStateTransitionAnimation { animation.play(workspaceAnim); } + final Drawable contentBackground = contentView.getBackground(); + final int alpha = contentBackground.getAlpha(); + // hideAppsCustomizeHelper is called in some cases when it is already hidden // don't perform all these no-op animations. In particularly, this was causing // the all-apps button to pop in and out. - if (fromView.getVisibility() == View.VISIBLE) { + if (doReveal && fromView.getVisibility() == View.VISIBLE) { int width = revealView.getMeasuredWidth(); int height = revealView.getMeasuredHeight(); float revealRadius = (float) Math.hypot(width / 2, height / 2); + contentBackground.setAlpha(0); revealView.setVisibility(View.VISIBLE); revealView.setAlpha(1f); revealView.setTranslationY(0); @@ -618,6 +641,8 @@ public class LauncherStateTransitionAnimation { dispatchOnLauncherTransitionPrepare(fromView, animated, true); dispatchOnLauncherTransitionPrepare(toView, animated, true); + } else { + fromView.setVisibility(View.GONE); } animation.addListener(new AnimatorListenerAdapter() { @@ -644,6 +669,7 @@ public class LauncherStateTransitionAnimation { contentView.setTranslationX(0); contentView.setTranslationY(0); contentView.setAlpha(1); + contentBackground.setAlpha(alpha); } if (overlaySearchBarView != null) { overlaySearchBarView.setAlpha(1f); diff --git a/src/com/android/launcher3/NetworkConnectionReceiver.java b/src/com/android/launcher3/NetworkConnectionReceiver.java new file mode 100644 index 000000000..0b49c208d --- /dev/null +++ b/src/com/android/launcher3/NetworkConnectionReceiver.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 The CyanogenMod 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; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; + +import java.util.HashSet; +import java.util.Set; + +public class NetworkConnectionReceiver extends BroadcastReceiver { + public static final String INTENT_ACTION = ConnectivityManager.CONNECTIVITY_ACTION; + + private final Set<NetworkStateChangeListener> mListeners; + + interface NetworkStateChangeListener { + void onNetworkConnected(); + void onNetworkDisconnected(); + } + + public NetworkConnectionReceiver() { + mListeners = new HashSet<NetworkStateChangeListener>(); + } + + public void registerListener(final NetworkStateChangeListener listener) { + mListeners.add(listener); + } + + public void deregisterListener(final NetworkStateChangeListener listener) { + mListeners.remove(listener); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!intent.getAction().equals(INTENT_ACTION)) return; + + boolean connected = Utilities.isNetworkConnected(context); + for (NetworkStateChangeListener listener: mListeners) { + if (connected) { + listener.onNetworkConnected(); + } else { + listener.onNetworkDisconnected(); + } + } + } +} diff --git a/src/com/android/launcher3/OverviewPanel.java b/src/com/android/launcher3/OverviewPanel.java new file mode 100644 index 000000000..2fdca6330 --- /dev/null +++ b/src/com/android/launcher3/OverviewPanel.java @@ -0,0 +1,32 @@ +package com.android.launcher3; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +public class OverviewPanel extends VerticalSlidingPanel implements Insettable { + public OverviewPanel(Context context) { + super(context); + } + + public OverviewPanel(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public OverviewPanel(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void setInsets(Rect insets) { + LinearLayout layout = (LinearLayout) + findViewById(R.id.settings_container); + FrameLayout.LayoutParams lp = + (FrameLayout.LayoutParams) layout.getLayoutParams(); + lp.bottomMargin = insets.bottom; + layout.setLayoutParams(lp); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/OverviewSettingsPanel.java b/src/com/android/launcher3/OverviewSettingsPanel.java new file mode 100644 index 000000000..8f1b435e8 --- /dev/null +++ b/src/com/android/launcher3/OverviewSettingsPanel.java @@ -0,0 +1,107 @@ +package com.android.launcher3; + +import android.content.res.Resources; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.widget.ListView; +import com.android.launcher3.list.PinnedHeaderListView; +import com.android.launcher3.list.SettingsPinnedHeaderAdapter; + +import java.util.ArrayList; +import java.util.Arrays; + +public class OverviewSettingsPanel { + public static final String ANDROID_SETTINGS = "com.android.settings"; + public static final String ANDROID_PROTECTED_APPS = + "com.android.settings.applications.ProtectedAppsActivity"; + public static final int HOME_SETTINGS_POSITION = 0; + public static final int DRAWER_SETTINGS_POSITION = 1; + public static final int APP_SETTINGS_POSITION = 2; + + private Launcher mLauncher; + private SettingsPinnedHeaderAdapter mSettingsAdapter; + private PinnedHeaderListView mListView; + + OverviewSettingsPanel(Launcher launcher) { + mLauncher = launcher; + } + + // One time initialization of the SettingsPinnedHeaderAdapter + public void initializeAdapter() { + // Settings pane Listview + mListView = (PinnedHeaderListView) mLauncher + .findViewById(R.id.settings_home_screen_listview); + mListView.setOverScrollMode(ListView.OVER_SCROLL_NEVER); + Resources res = mLauncher.getResources(); + String[] headers = new String[] { + res.getString(R.string.home_screen_settings), + res.getString(R.string.drawer_settings), + res.getString(R.string.app_settings)}; + + String[] valuesApp = new String[] { + res.getString(R.string.larger_icons_text), + res.getString(R.string.protected_app_settings)}; + + mSettingsAdapter = new SettingsPinnedHeaderAdapter(mLauncher); + mSettingsAdapter.setHeaders(headers); + mSettingsAdapter.addPartition(false, true); + mSettingsAdapter.addPartition(false, true); + mSettingsAdapter.addPartition(false, true); + mSettingsAdapter.mPinnedHeaderCount = headers.length; + + mSettingsAdapter.changeCursor(HOME_SETTINGS_POSITION, + createCursor(headers[0], getValuesHome())); + mSettingsAdapter.changeCursor(DRAWER_SETTINGS_POSITION, + createCursor(headers[1], getValuesDrawer())); + mSettingsAdapter.changeCursor(APP_SETTINGS_POSITION, createCursor(headers[2], valuesApp)); + mListView.setAdapter(mSettingsAdapter); + } + + private Cursor createCursor(String header, String[] values) { + MatrixCursor cursor = new MatrixCursor(new String[]{"_id", header}); + int count = values.length; + for (int i = 0; i < count; i++) { + cursor.addRow(new Object[]{i, values[i]}); + } + return cursor; + } + + private String[] getValuesHome() { + Resources res = mLauncher.getResources(); + ArrayList<String> values = new ArrayList<String>(Arrays.asList(new String[]{ + res.getString(R.string.home_screen_search_text), + res.getString(R.string.icon_labels), + res.getString(R.string.scrolling_wallpaper), + res.getString(R.string.grid_size_text), + res.getString(R.string.allow_rotation_title)})); + + // Add additional external settings. + RemoteFolderManager.onInitializeHomeSettings(values, mLauncher); + + String[] valuesArr = new String[values.size()]; + values.toArray(valuesArr); + return valuesArr; + } + + private String[] getValuesDrawer() { + Resources res = mLauncher.getResources(); + ArrayList<String> values = new ArrayList<String>(Arrays.asList(new String[]{ + res.getString(R.string.icon_labels), + res.getString(R.string.app_drawer_style), + res.getString(R.string.app_drawer_color), + res.getString(R.string.fast_scroller), + res.getString(R.string.fast_scroller_type), + res.getString(R.string.home_screen_search_text)})); + + // Add additional external settings. + RemoteFolderManager.onInitializeDrawerSettings(values, mLauncher); + + String[] valuesArr = new String[values.size()]; + values.toArray(valuesArr); + return valuesArr; + } + + public void notifyDataSetInvalidated() { + mSettingsAdapter.notifyDataSetInvalidated(); + } +} diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 05f0a0553..fe940c182 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -169,7 +169,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc @Thunk PageIndicator mPageIndicator; // The viewport whether the pages are to be contained (the actual view may be larger than the // viewport) - private Rect mViewport = new Rect(); + protected Rect mViewport = new Rect(); // Reordering // We use the min scale to determine how much to expand the actually PagedView measured @@ -1238,15 +1238,24 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { // XXX-RTL: This will be fixed in a future CL if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) { - getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode); + View page = getPageAt(mCurrentPage); + if (page != null) { + page.addFocusables(views, direction, focusableMode); + } } if (direction == View.FOCUS_LEFT) { if (mCurrentPage > 0) { - getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode); + View page = getPageAt(mCurrentPage - 1); + if (page != null) { + page.addFocusables(views, direction, focusableMode); + } } } else if (direction == View.FOCUS_RIGHT){ if (mCurrentPage < getPageCount() - 1) { - getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode); + View page = getPageAt(mCurrentPage + 1); + if (page != null) { + page.addFocusables(views, direction, focusableMode); + } } } } diff --git a/src/com/android/launcher3/ProtectedComponentsHelper.java b/src/com/android/launcher3/ProtectedComponentsHelper.java new file mode 100644 index 000000000..12ec96da4 --- /dev/null +++ b/src/com/android/launcher3/ProtectedComponentsHelper.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2015 The CyanogenMod 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; + +import android.content.ComponentName; +import android.content.Context; +import cyanogenmod.providers.CMSettings; + +import java.util.ArrayList; + +public class ProtectedComponentsHelper { + private static final int FILTER_APPS_SYSTEM_FLAG = 1; + private static final int FILTER_APPS_DOWNLOADED_FLAG = 2; + private static int sFilterApps = FILTER_APPS_SYSTEM_FLAG | FILTER_APPS_DOWNLOADED_FLAG; + + private static ArrayList<ComponentName> sProtectedApps = new ArrayList<ComponentName>(); + private static ArrayList<String> sProtectedPackages = new ArrayList<String>(); + + /** + * Gets the list of protected components from {@link CMSettings} and updates the existing list + * of protected apps and packages + * @param context Context + */ + public static void updateProtectedComponentsLists(Context context) { + String protectedComponents = CMSettings.Secure.getString(context.getContentResolver(), + CMSettings.Secure.PROTECTED_COMPONENTS); + protectedComponents = protectedComponents == null ? "" : protectedComponents; + String [] flattened = protectedComponents.split("\\|"); + sProtectedApps = new ArrayList<ComponentName>(flattened.length); + sProtectedPackages = new ArrayList<String>(flattened.length); + for (String flat : flattened) { + ComponentName cmp = ComponentName.unflattenFromString(flat); + if (cmp != null) { + sProtectedApps.add(cmp); + sProtectedPackages.add(cmp.getPackageName()); + } + } + } + + /** + * Checks if the given combination of {@link ComponentName} and flags is for a protected app + */ + public static boolean isProtectedApp(int flags, ComponentName componentName) { + boolean system = isSystemFlag(flags); + return sProtectedApps.contains(componentName) || (system && !getShowSystemApps()) || + (!system && !getShowDownloadedApps()); + } + + /** + * Checks if the given combination of package name and flags is for a protected package + */ + public static boolean isProtectedPackage(int flags, String packageName) { + boolean system = isSystemFlag(flags); + return (sProtectedPackages.contains(packageName) || (system && !getShowSystemApps()) || + (!system && !getShowDownloadedApps())); + } + + private static boolean isSystemFlag(int flags) { + return (flags & AppInfo.DOWNLOADED_FLAG) == 0; + } + + private static boolean getShowSystemApps() { + return (sFilterApps & FILTER_APPS_SYSTEM_FLAG) != 0; + } + + private static boolean getShowDownloadedApps() { + return (sFilterApps & FILTER_APPS_DOWNLOADED_FLAG) != 0; + } +} diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java index 772a334b9..182d741ae 100644 --- a/src/com/android/launcher3/SearchDropTargetBar.java +++ b/src/com/android/launcher3/SearchDropTargetBar.java @@ -64,6 +64,7 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D private static final AccelerateInterpolator sAccelerateInterpolator = new AccelerateInterpolator(); + private Launcher mLauncher; private State mState = State.SEARCH_BAR; @Thunk View mQSB; @Thunk View mDropTargetBar; @@ -84,6 +85,8 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D } public void setup(Launcher launcher, DragController dragController) { + mLauncher = launcher; + dragController.addDragListener(this); dragController.setFlingToDeleteDropTarget(mDeleteDropTarget); @@ -134,24 +137,39 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D }); } + public void setupQsb(Launcher launcher) { + mLauncher = launcher; + mQSB = launcher.getOrCreateQsbBar(); + } + public void setQsbSearchBar(View qsb) { + float alpha = 1f; + int visibility = View.VISIBLE; + if (mQSB != null) { + alpha = mQSB.getAlpha(); + visibility = mQSB.getVisibility(); + } + mQSB = qsb; if (mQSB != null) { - // Update the search ber animation + mQSB.setAlpha(alpha); + mQSB.setVisibility(visibility); + + // Update the search bar animation mQSBSearchBarAnimator = new LauncherViewPropertyAnimator(mQSB); mQSBSearchBarAnimator.setInterpolator(sAccelerateInterpolator); mQSBSearchBarAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { // Ensure that the view is visible for the animation - if (mQSB != null) { + if (mQSB != null && isSearchBarVisible()) { mQSB.setVisibility(View.VISIBLE); } } @Override public void onAnimationEnd(Animator animation) { - if (mQSB != null) { + if (mQSB != null && isSearchBarVisible()) { AlphaUpdateListener.updateVisibility(mQSB, mAccessibilityEnabled); } } @@ -173,9 +191,10 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D AccessibilityManager am = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); mAccessibilityEnabled = am.isEnabled(); - - animateViewAlpha(mQSBSearchBarAnimator, mQSB, newState.getSearchBarAlpha(), - duration); + if (mLauncher.getDeviceProfile().searchBarVisible) { + animateViewAlpha(mQSBSearchBarAnimator, mQSB, newState.getSearchBarAlpha(), + duration); + } animateViewAlpha(mDropTargetBarAnimator, mDropTargetBar, newState.getDropTargetBarAlpha(), duration); } @@ -245,8 +264,15 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D } } + public boolean isSearchBarVisible() { + if (mLauncher != null) { + return mLauncher.getDeviceProfile().searchBarVisible; + } + return true; + } + public void enableAccessibleDrag(boolean enable) { - if (mQSB != null) { + if (mQSB != null && isSearchBarVisible()) { mQSB.setVisibility(enable ? View.GONE : View.VISIBLE); } mInfoDropTarget.enableAccessibleDrag(enable); diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java deleted file mode 100644 index dab71c862..000000000 --- a/src/com/android/launcher3/SettingsActivity.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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; - -import android.app.Activity; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.PreferenceFragment; -import android.preference.SwitchPreference; - -/** - * Settings activity for Launcher. Currently implements the following setting: Allow rotation - */ -public class SettingsActivity extends Activity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Display the fragment as the main content. - getFragmentManager().beginTransaction() - .replace(android.R.id.content, new LauncherSettingsFragment()) - .commit(); - } - - /** - * This fragment shows the launcher preferences. - */ - public static class LauncherSettingsFragment extends PreferenceFragment - implements OnPreferenceChangeListener { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.launcher_preferences); - - SwitchPreference pref = (SwitchPreference) findPreference( - Utilities.ALLOW_ROTATION_PREFERENCE_KEY); - pref.setPersistent(false); - - Bundle extras = new Bundle(); - extras.putBoolean(LauncherSettings.Settings.EXTRA_DEFAULT_VALUE, false); - Bundle value = getActivity().getContentResolver().call( - LauncherSettings.Settings.CONTENT_URI, - LauncherSettings.Settings.METHOD_GET_BOOLEAN, - Utilities.ALLOW_ROTATION_PREFERENCE_KEY, extras); - pref.setChecked(value.getBoolean(LauncherSettings.Settings.EXTRA_VALUE)); - - pref.setOnPreferenceChangeListener(this); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - Bundle extras = new Bundle(); - extras.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, (Boolean) newValue); - getActivity().getContentResolver().call( - LauncherSettings.Settings.CONTENT_URI, - LauncherSettings.Settings.METHOD_SET_BOOLEAN, - preference.getKey(), extras); - return true; - } - } -} diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 5766cf2f2..b1a787bfc 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -100,7 +100,7 @@ public class ShortcutInfo extends ItemInfo { /** * The application icon. */ - private Bitmap mIcon; + Bitmap mIcon; /** * Indicates that the icon is disabled due to safe mode restrictions. @@ -265,6 +265,14 @@ public class ShortcutInfo extends ItemInfo { return (status & flag) != 0; } + /** + * Check if this shortcut has a specific flag. + * @param flag flag to check. + * @return true if the flag is present, false otherwise. + */ + public boolean hasFlag(int flag) { + return (flags & flag) != 0; + } public final boolean isPromise() { return hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINTALL_ICON); diff --git a/src/com/android/launcher3/ThemeChangedReceiver.java b/src/com/android/launcher3/ThemeChangedReceiver.java new file mode 100644 index 000000000..3f4f3af38 --- /dev/null +++ b/src/com/android/launcher3/ThemeChangedReceiver.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 The CyanogenMod 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; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import java.io.File; +import java.util.ArrayList; + +public class ThemeChangedReceiver extends BroadcastReceiver { + private static final String EXTRA_COMPONENTS = "components"; + + public static final String MODIFIES_ICONS = "mods_icons"; + public static final String MODIFIES_FONTS = "mods_fonts"; + public static final String MODIFIES_OVERLAYS = "mods_overlays"; + + public void onReceive(Context context, Intent intent) { + // components is a string array of the components that changed + ArrayList<String> components = intent.getStringArrayListExtra(EXTRA_COMPONENTS); + if (isInterestingThemeChange(components)) { + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app == null) { + LauncherAppState.setApplicationContext(context); + app = LauncherAppState.getInstance(); + } + clearAppIconCache(context); + clearWidgetPreviewCache(context); + app.recreateWidgetPreviewDb(); + app.getIconCache().flush(); + app.getModel().forceReload(); + + if (Launcher.sRemoteFolderManager != null) { + Launcher.sRemoteFolderManager.onThemeChanged(); + } + } + } + + /** + * We consider this an "interesting" theme change if it modifies icons, overlays, or fonts. + * @param components + * @return + */ + private boolean isInterestingThemeChange(ArrayList<String> components) { + if (components != null) { + for (String component : components) { + if (component.equals(MODIFIES_ICONS) || + component.equals(MODIFIES_FONTS) || + component.equals(MODIFIES_OVERLAYS)) { + return true; + } + } + } + return false; + } + + private void clearWidgetPreviewCache(Context context) { + context.deleteDatabase(LauncherFiles.WIDGET_PREVIEWS_DB); + } + + private void clearAppIconCache(Context context) { + context.deleteDatabase(LauncherFiles.APP_ICONS_DB); + } +} diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index adedd33b2..bf11aa19e 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -25,7 +25,6 @@ import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -44,6 +43,8 @@ import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.os.Build; import android.os.Bundle; import android.os.Process; @@ -56,6 +57,8 @@ import android.util.TypedValue; import android.view.View; import android.widget.Toast; +import com.android.launcher3.settings.SettingsProvider; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; @@ -110,22 +113,19 @@ public final class Utilities { private static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); - public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation"; - public static boolean isPropertyEnabled(String propertyName) { return Log.isLoggable(propertyName, Log.VERBOSE); } public static boolean isAllowRotationPrefEnabled(Context context, boolean multiProcess) { - SharedPreferences sharedPrefs = context.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE | (multiProcess ? - Context.MODE_MULTI_PROCESS : 0)); - boolean allowRotationPref = sharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false); + boolean allowRotationPref = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_ALLOW_ROTATION, + R.bool.preferences_interface_allow_rotation); return sForceEnableRotation || allowRotationPref; } public static boolean isRotationAllowedForDevice(Context context) { - return sForceEnableRotation || context.getResources().getBoolean(R.bool.allow_rotation); + return sForceEnableRotation || context.getResources().getBoolean(R.bool.preferences_interface_allow_rotation); } public static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) { @@ -695,8 +695,7 @@ public final class Utilities { } public static float dpiFromPx(int size, DisplayMetrics metrics){ - float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; - return (size / densityRatio); + return (size / metrics.density); } public static int pxFromDp(float size, DisplayMetrics metrics) { return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, @@ -710,4 +709,33 @@ public final class Utilities { public static String createDbSelectionQuery(String columnName, Iterable<?> values) { return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, TextUtils.join(", ", values)); } + + public static boolean searchActivityExists(Context context) { + final SearchManager searchManager = + (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); + ComponentName globalSearchActivity = searchManager.getGlobalSearchActivity(); + + return globalSearchActivity != null; + } + + public static boolean isPackageInstalled(Context context, String pkg) { + PackageManager packageManager = context.getPackageManager(); + try { + PackageInfo pi = packageManager.getPackageInfo(pkg, 0); + return pi.applicationInfo.enabled; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + public static boolean isNetworkConnected(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo(); + return activeNetwork != null && activeNetwork.isConnected(); + } + + public static boolean isConnectedToWiFi(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + return connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isConnected(); + } } diff --git a/src/com/android/launcher3/VerticalSlidingPanel.java b/src/com/android/launcher3/VerticalSlidingPanel.java new file mode 100644 index 000000000..88a52392c --- /dev/null +++ b/src/com/android/launcher3/VerticalSlidingPanel.java @@ -0,0 +1,1330 @@ +package com.android.launcher3; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.ViewDragHelper; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.Animation; +import android.view.animation.TranslateAnimation; + +public class VerticalSlidingPanel extends ViewGroup { + private static final String TAG = VerticalSlidingPanel.class.getSimpleName(); + + /** + * Default peeking out panel height + */ + private static final int DEFAULT_PANEL_HEIGHT = 68; // dp; + + /** + * Default height of the shadow above the peeking out panel + */ + private static final int DEFAULT_SHADOW_HEIGHT = 4; // dp; + + /** + * If no fade color is given by default it will fade to 80% gray. + */ + private static final int DEFAULT_FADE_COLOR = 0x99000000; + + /** + * Default Minimum velocity that will be detected as a fling + */ + private static final int DEFAULT_MIN_FLING_VELOCITY = 400; // dips per second + /** + * Default is set to false because that is how it was written + */ + private static final boolean DEFAULT_OVERLAY_FLAG = false; + /** + * Default attributes for layout + */ + private static final int[] DEFAULT_ATTRS = new int[] { + android.R.attr.gravity + }; + + /** + * Minimum velocity that will be detected as a fling + */ + private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY; + + /** + * The fade color used for the panel covered by the slider. 0 = no fading. + */ + private int mCoveredFadeColor = DEFAULT_FADE_COLOR; + + /** + * Default parallax length of the main view + */ + private static final int DEFAULT_PARALLAX_OFFSET = 0; + + /** + * The paint used to dim the main layout when sliding + */ + private final Paint mCoveredFadePaint = new Paint(); + + /** + * Drawable used to draw the shadow between panes. + */ + private final Drawable mShadowDrawable; + + /** + * The size of the overhang in pixels. + */ + private int mPanelHeight = -1; + + /** + * The size of the shadow in pixels. + */ + private int mShadowHeight = -1; + + /** + * Parallax offset + */ + private int mParallaxOffset = -1; + + /** + * True if the collapsed panel should be dragged up. + */ + private boolean mIsSlidingUp; + + /** + * True if a panel can slide with the current measurements + */ + private boolean mCanSlide; + + /** + * Panel overlays the windows instead of putting it underneath it. + */ + private boolean mOverlayContent = DEFAULT_OVERLAY_FLAG; + + /** + * If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be + * used for dragging. + */ + private View mDragView; + + /** + * If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be + * used for dragging. + */ + private int mDragViewResId = -1; + + /** + * The child view that can slide, if any. + */ + private View mSlideableView; + + /** + * The main view + */ + private View mMainView; + + /** + * Current state of the slideable view. + */ + private enum SlideState { + EXPANDED, + COLLAPSED, + ANCHORED + } + private SlideState mSlideState = SlideState.COLLAPSED; + + /** + * How far the panel is offset from its expanded position. + * range [0, 1] where 0 = expanded, 1 = collapsed. + */ + private float mSlideOffset; + + /** + * How far in pixels the slideable panel may move. + */ + private int mSlideRange; + + /** + * A panel view is locked into internal scrolling or another condition that + * is preventing a drag. + */ + private boolean mIsUnableToDrag; + + /** + * Flag indicating that sliding feature is enabled\disabled + */ + private boolean mIsSlidingEnabled; + + /** + * Flag indicating if a drag view can have its own touch events. If set + * to true, a drag view can scroll horizontally and have its own click listener. + * + * Default is set to false. + */ + private boolean mIsUsingDragViewTouchEvents; + + /** + * Threshold to tell if there was a scroll touch event. + */ + private final int mScrollTouchSlop; + + private float mInitialMotionX; + private float mInitialMotionY; + private float mAnchorPoint = 0.f; + private TranslateAnimation mAnimation; + + private PanelSlideListener mPanelSlideListener; + + private final ViewDragHelper mDragHelper; + + /** + * Stores whether or not the pane was expanded the last time it was slideable. + * If expand/collapse operations are invoked this state is modified. Used by + * instance state save/restore. + */ + private boolean mFirstLayout = true; + + private final Rect mTmpRect = new Rect(); + + /** + * Listener for monitoring events about sliding panes. + */ + public interface PanelSlideListener { + /** + * Called when a sliding pane's position changes. + * @param panel The child view that was moved + * @param slideOffset The new offset of this sliding pane within its range, from 0-1 + */ + public void onPanelSlide(View panel, float slideOffset); + /** + * Called when a sliding pane becomes slid completely collapsed. The pane may or may not + * be interactive at this point depending on if it's shown or hidden + * @param panel The child view that was slid to an collapsed position, revealing other panes + */ + public void onPanelCollapsed(View panel); + + /** + * Called when a sliding pane becomes slid completely expanded. The pane is now guaranteed + * to be interactive. It may now obscure other views in the layout. + * @param panel The child view that was slid to a expanded position + */ + public void onPanelExpanded(View panel); + + public void onPanelAnchored(View panel); + + public void onPanelShown(View panel); + } + + /** + * No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset + * of the listener methods you can extend this instead of implement the full interface. + */ + public static class SimplePanelSlideListener implements PanelSlideListener { + @Override + public void onPanelSlide(View panel, float slideOffset) { + } + @Override + public void onPanelCollapsed(View panel) { + } + @Override + public void onPanelExpanded(View panel) { + } + @Override + public void onPanelAnchored(View panel) { + } + @Override + public void onPanelShown(View panel) { + } + } + + public VerticalSlidingPanel(Context context) { + this(context, null); + } + + public VerticalSlidingPanel(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public VerticalSlidingPanel(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (attrs != null) { + TypedArray defAttrs = context.obtainStyledAttributes(attrs, DEFAULT_ATTRS); + + if (defAttrs != null) { + int gravity = defAttrs.getInt(0, Gravity.NO_GRAVITY); + if (gravity != Gravity.TOP && gravity != Gravity.BOTTOM) { + throw new IllegalArgumentException("gravity must be set to either top or bottom"); + } + mIsSlidingUp = gravity == Gravity.BOTTOM; + } + + defAttrs.recycle(); + + TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.VerticalSlidingPanel); + + if (ta != null) { + mPanelHeight = ta.getDimensionPixelSize(R.styleable.VerticalSlidingPanel_panelHeight, -1); + mShadowHeight = ta.getDimensionPixelSize(R.styleable.VerticalSlidingPanel_shadowHeight, -1); + mParallaxOffset = ta.getDimensionPixelSize(R.styleable.VerticalSlidingPanel_parallaxOffset, -1); + + mMinFlingVelocity = ta.getInt(R.styleable.VerticalSlidingPanel_flingVelocity, DEFAULT_MIN_FLING_VELOCITY); + mCoveredFadeColor = ta.getColor(R.styleable.VerticalSlidingPanel_fadeColor, DEFAULT_FADE_COLOR); + + mDragViewResId = ta.getResourceId(R.styleable.VerticalSlidingPanel_dragView, -1); + + mOverlayContent = ta.getBoolean(R.styleable.VerticalSlidingPanel_overlay,DEFAULT_OVERLAY_FLAG); + } + + ta.recycle(); + } + + final float density = context.getResources().getDisplayMetrics().density; + if (mPanelHeight == -1) { + mPanelHeight = (int) (DEFAULT_PANEL_HEIGHT * density + 0.5f); + } + if (mShadowHeight == -1) { + mShadowHeight = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f); + } + if (mParallaxOffset == -1) { + mParallaxOffset = (int) (DEFAULT_PARALLAX_OFFSET * density); + } + // If the shadow height is zero, don't show the shadow + if (mShadowHeight > 0) { + if (mIsSlidingUp) { + mShadowDrawable = getResources().getDrawable(R.drawable.above_shadow, + context.getTheme()); + } else { + mShadowDrawable = getResources().getDrawable(R.drawable.below_shadow, + context.getTheme()); + } + + } else { + mShadowDrawable = null; + } + + setWillNotDraw(false); + + mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); + mDragHelper.setMinVelocity(mMinFlingVelocity * density); + + mCanSlide = true; + mIsSlidingEnabled = true; + + ViewConfiguration vc = ViewConfiguration.get(context); + mScrollTouchSlop = vc.getScaledTouchSlop(); + } + + /** + * Set the Drag View after the view is inflated + */ + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (mDragViewResId != -1) { + mDragView = findViewById(mDragViewResId); + } + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + if (mPanelSlideListener != null && visibility == VISIBLE) { + mPanelSlideListener.onPanelShown(changedView); + } + } + + /** + * Set the color used to fade the pane covered by the sliding pane out when the pane + * will become fully covered in the expanded state. + * + * @param color An ARGB-packed color value + */ + public void setCoveredFadeColor(int color) { + mCoveredFadeColor = color; + invalidate(); + } + + /** + * @return The ARGB-packed color value used to fade the fixed pane + */ + public int getCoveredFadeColor() { + return mCoveredFadeColor; + } + + /** + * Set sliding enabled flag + * @param enabled flag value + */ + public void setSlidingEnabled(boolean enabled) { + mIsSlidingEnabled = enabled; + } + + /** + * Set the collapsed panel height in pixels + * + * @param val A height in pixels + */ + public void setPanelHeight(int val) { + mPanelHeight = val; + requestLayout(); + } + + /** + * @return The current collapsed panel height + */ + public int getPanelHeight() { + return mPanelHeight; + } + + /** + * @return The current parallax offset + */ + public int getCurrentParallaxOffset() { + int offset = (int)(mParallaxOffset * (1 - mSlideOffset)); + return mIsSlidingUp ? -offset : offset; + } + + /** + * Sets the panel slide listener + * @param listener + */ + public void setPanelSlideListener(PanelSlideListener listener) { + mPanelSlideListener = listener; + } + + /** + * Set the draggable view portion. Use to null, to allow the whole panel to be draggable + * + * @param dragView A view that will be used to drag the panel. + */ + public void setDragView(View dragView) { + mDragView = dragView; + } + + /** + * Set an anchor point where the panel can stop during sliding + * + * @param anchorPoint A value between 0 and 1, determining the position of the anchor point + * starting from the top of the layout. + */ + public void setAnchorPoint(float anchorPoint) { + if (anchorPoint > 0 && anchorPoint < 1) + mAnchorPoint = anchorPoint; + } + + /** + * Sets whether or not the panel overlays the content + * @param overlayed + */ + public void setOverlayed(boolean overlayed) { + mOverlayContent = overlayed; + } + + /** + * Check if the panel is set as an overlay. + */ + public boolean isOverlayed() { + return mOverlayContent; + } + + void dispatchOnPanelSlide(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelSlide(panel, mSlideOffset); + } + } + + void dispatchOnPanelExpanded(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelExpanded(panel); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void dispatchOnPanelCollapsed(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelCollapsed(panel); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void dispatchOnPanelAnchored(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelAnchored(panel); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void updateObscuredViewVisibility() { + if (getChildCount() == 0) { + return; + } + final int leftBound = getPaddingLeft(); + final int rightBound = getWidth() - getPaddingRight(); + final int topBound = getPaddingTop(); + final int bottomBound = getHeight() - getPaddingBottom(); + final int left; + final int right; + final int top; + final int bottom; + if (mSlideableView != null && hasOpaqueBackground(mSlideableView)) { + left = mSlideableView.getLeft(); + right = mSlideableView.getRight(); + top = mSlideableView.getTop(); + bottom = mSlideableView.getBottom(); + } else { + left = right = top = bottom = 0; + } + View child = getChildAt(0); + final int clampedChildLeft = Math.max(leftBound, child.getLeft()); + final int clampedChildTop = Math.max(topBound, child.getTop()); + final int clampedChildRight = Math.min(rightBound, child.getRight()); + final int clampedChildBottom = Math.min(bottomBound, child.getBottom()); + final int vis; + if (clampedChildLeft >= left && clampedChildTop >= top && + clampedChildRight <= right && clampedChildBottom <= bottom) { + vis = INVISIBLE; + } else { + vis = VISIBLE; + } + child.setVisibility(vis); + } + + void setAllChildrenVisible() { + for (int i = 0, childCount = getChildCount(); i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == INVISIBLE) { + child.setVisibility(VISIBLE); + } + } + } + + private static boolean hasOpaqueBackground(View v) { + final Drawable bg = v.getBackground(); + if (bg != null) { + return bg.getOpacity() == PixelFormat.OPAQUE; + } + return false; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException("Width must have an exact value or MATCH_PARENT"); + } else if (heightMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException("Height must have an exact value or MATCH_PARENT"); + } + + int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); + int panelHeight = mPanelHeight; + + final int childCount = getChildCount(); + + if (childCount > 2) { + Log.e(TAG, "onMeasure: More than two child views are not supported."); + } else if (getChildAt(1) != null && getChildAt(1).getVisibility() == GONE) { + panelHeight = 0; + } + + // We'll find the current one below. + mSlideableView = null; + mCanSlide = false; + + // First pass. Measure based on child LayoutParams width/height. + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int height = layoutHeight; + if (child.getVisibility() == GONE) { + lp.dimWhenOffset = false; + continue; + } + + if (i == 1) { + lp.slideable = true; + lp.dimWhenOffset = true; + mSlideableView = child; + mCanSlide = true; + } else { + if (!mOverlayContent) { + height -= panelHeight; + } + mMainView = child; + } + + int childWidthSpec; + if (lp.width == LayoutParams.WRAP_CONTENT) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); + } else if (lp.width == LayoutParams.MATCH_PARENT) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + } else { + childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); + } + + int childHeightSpec; + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); + } else if (lp.height == LayoutParams.MATCH_PARENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); + } + + child.measure(childWidthSpec, childHeightSpec); + } + + setMeasuredDimension(widthSize, heightSize); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int paddingLeft = getPaddingLeft(); + final int paddingTop = getPaddingTop(); + final int slidingTop = getSlidingTop(); + + final int childCount = getChildCount(); + + if (mFirstLayout) { + switch (mSlideState) { + case EXPANDED: + mSlideOffset = mCanSlide ? 0.f : 1.f; + break; + case ANCHORED: + mSlideOffset = mCanSlide ? mAnchorPoint : 1.f; + break; + default: + mSlideOffset = 1.f; + break; + } + } + + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final int childHeight = child.getMeasuredHeight(); + + if (lp.slideable) { + mSlideRange = childHeight - mPanelHeight; + } + + int childTop; + if (mIsSlidingUp) { + childTop = lp.slideable ? slidingTop + (int) (mSlideRange * mSlideOffset) : paddingTop; + } else { + childTop = lp.slideable ? slidingTop - (int) (mSlideRange * mSlideOffset) : paddingTop; + if (!lp.slideable && !mOverlayContent) { + childTop += mPanelHeight; + } + } + final int childBottom = childTop + childHeight; + final int childLeft = paddingLeft; + final int childRight = childLeft + child.getMeasuredWidth(); + + child.layout(childLeft, childTop, childRight, childBottom); + } + + if (mFirstLayout) { + updateObscuredViewVisibility(); + } + + mFirstLayout = false; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + // Recalculate sliding panes and their details + if (h != oldh) { + mFirstLayout = true; + } + } + + /** + * Set if the drag view can have its own touch events. If set + * to true, a drag view can scroll horizontally and have its own click listener. + * + * Default is set to false. + */ + public void setEnableDragViewTouchEvents(boolean enabled) { + mIsUsingDragViewTouchEvents = enabled; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + + if (mAnimation != null || !mCanSlide || !mIsSlidingEnabled || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { + mDragHelper.cancel(); + return super.onInterceptTouchEvent(ev); + } + + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + mDragHelper.cancel(); + return false; + } + + final float x = ev.getX(); + final float y = ev.getY(); + boolean interceptTap = false; + + switch (action) { + case MotionEvent.ACTION_DOWN: { + mIsUnableToDrag = false; + mInitialMotionX = x; + mInitialMotionY = y; + if (isDragViewUnder((int) x, (int) y) && !mIsUsingDragViewTouchEvents) { + interceptTap = true; + } + break; + } + + case MotionEvent.ACTION_MOVE: { + final float adx = Math.abs(x - mInitialMotionX); + final float ady = Math.abs(y - mInitialMotionY); + final int dragSlop = mDragHelper.getTouchSlop(); + + // Handle any horizontal scrolling on the drag view. + if (mIsUsingDragViewTouchEvents) { + if (adx > mScrollTouchSlop && ady < mScrollTouchSlop) { + return super.onInterceptTouchEvent(ev); + } + // Intercept the touch if the drag view has any vertical scroll. + // onTouchEvent will determine if the view should drag vertically. + else if (ady > mScrollTouchSlop) { + interceptTap = isDragViewUnder((int) x, (int) y); + } + } + + if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int) x, (int) y)) { + mDragHelper.cancel(); + mIsUnableToDrag = true; + return false; + } + break; + } + } + + final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev); + + return interceptForDrag; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!mCanSlide || !mIsSlidingEnabled || mAnimation != null) { + return super.onTouchEvent(ev); + } + + mDragHelper.processTouchEvent(ev); + + final int action = ev.getAction(); + boolean wantTouchEvents = true; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + + //Fix to allow both SettingPanel Drag and Workspace Drag + if (mSlideState == SlideState.COLLAPSED) { + if (y < mSlideableView.getTop()) { + return false; + } + } + + mInitialMotionX = x; + mInitialMotionY = y; + break; + } + + case MotionEvent.ACTION_UP: { + final float x = ev.getX(); + final float y = ev.getY(); + final float dx = x - mInitialMotionX; + final float dy = y - mInitialMotionY; + final int slop = mDragHelper.getTouchSlop(); + View dragView = mDragView != null ? mDragView : mSlideableView; + if (dx * dx + dy * dy < slop * slop && + isDragViewUnder((int) x, (int) y)) { + dragView.playSoundEffect(SoundEffectConstants.CLICK); + if (!isExpanded() && !isAnchored()) { + expandPane(mAnchorPoint); + } else { + collapsePane(); + } + break; + } + break; + } + } + + return wantTouchEvents; + } + + private boolean isDragViewUnder(int x, int y) { + View dragView = mDragView != null ? mDragView : mSlideableView; + if (dragView == null) return false; + int[] viewLocation = new int[2]; + dragView.getLocationOnScreen(viewLocation); + int[] parentLocation = new int[2]; + this.getLocationOnScreen(parentLocation); + int screenX = parentLocation[0] + x; + int screenY = parentLocation[1] + y; + return screenX >= viewLocation[0] && screenX < viewLocation[0] + dragView.getWidth() && + screenY >= viewLocation[1] && screenY < viewLocation[1] + dragView.getHeight(); + } + + private boolean expandPane(View pane, int initialVelocity, float mSlideOffset) { + if (mFirstLayout || smoothSlideTo(mSlideOffset, initialVelocity)) { + return true; + } + return false; + } + + private boolean collapsePane(View pane, int initialVelocity) { + if (mFirstLayout || smoothSlideTo(1.f, initialVelocity)) { + return true; + } + return false; + } + + private int getSlidingTop() { + if (mSlideableView != null) { + return mIsSlidingUp + ? getMeasuredHeight() - getPaddingBottom() - mSlideableView.getMeasuredHeight() + : getPaddingTop(); + } + + return getMeasuredHeight() - getPaddingBottom(); + } + + /** + * Collapse the sliding pane if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now collapsed/in the process of collapsing + */ + public boolean collapsePane() { + return collapsePane(mSlideableView, 0); + } + + /** + * Expand the sliding pane if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now expanded/in the process of expading + */ + public boolean expandPane() { + return expandPane(0); + } + + /** + * Partially expand the sliding pane up to a specific offset + * + * @param mSlideOffset Value between 0 and 1, where 0 is completely expanded. + * @return true if the pane was slideable and is now expanded/in the process of expading + */ + public boolean expandPane(float mSlideOffset) { + if (!isPaneVisible()) { + showPane(); + } + return expandPane(mSlideableView, 0, mSlideOffset); + } + + /** + * Check if the layout is completely expanded. + * + * @return true if sliding panels are completely expanded + */ + public boolean isExpanded() { + return mSlideState == SlideState.EXPANDED; + } + + /** + * Check if the layout is anchored in an intermediate point. + * + * @return true if sliding panels are anchored + */ + public boolean isAnchored() { + return mSlideState == SlideState.ANCHORED; + } + + /** + * Check if the content in this layout cannot fully fit side by side and therefore + * the content pane can be slid back and forth. + * + * @return true if content in this layout can be expanded + */ + public boolean isSlideable() { + return mCanSlide; + } + + public boolean isPaneVisible() { + if (getChildCount() < 2) { + return false; + } + View slidingPane = getChildAt(1); + return slidingPane.getVisibility() == View.VISIBLE; + } + + public void showPane() { + if (getChildCount() < 2) { + return; + } + final View slidingPane = getChildAt(1); + mAnimation = new TranslateAnimation(0, 0, (mIsSlidingUp ? 1 : -1) * getPanelHeight(), 0); + mAnimation.setDuration(400); + mAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + slidingPane.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animation animation) { + requestLayout(); + mAnimation = null; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + slidingPane.startAnimation(mAnimation); + } + + public void hidePane() { + if (mSlideableView == null) { + return; + } + mAnimation = new TranslateAnimation(0, 0, 0, (mIsSlidingUp ? 1 : -1) * getPanelHeight()); + mAnimation.setDuration(500); + mAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + + } + + @Override + public void onAnimationEnd(Animation animation) { + mSlideableView.setVisibility(View.GONE); + requestLayout(); + mAnimation = null; + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + }); + mSlideableView.startAnimation(mAnimation); + } + + private void onPanelDragged(int newTop) { + final int topBound = getSlidingTop(); + mSlideOffset = mIsSlidingUp + ? (float) (newTop - topBound) / mSlideRange + : (float) (topBound - newTop) / mSlideRange; + dispatchOnPanelSlide(mSlideableView); + + if (mParallaxOffset > 0) { + int mainViewOffset = getCurrentParallaxOffset(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mMainView.setTranslationY(mainViewOffset); + } else { + mMainView.animate().translationY(mainViewOffset); + } + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + boolean result; + final int save = canvas.save(Canvas.CLIP_SAVE_FLAG); + + boolean drawScrim = false; + + if (mCanSlide && !lp.slideable && mSlideableView != null) { + // Clip against the slider; no sense drawing what will immediately be covered, + // Unless the panel is set to overlay content + if (!mOverlayContent) { + canvas.getClipBounds(mTmpRect); + if (mIsSlidingUp) { + mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop()); + } else { + mTmpRect.top = Math.max(mTmpRect.top, mSlideableView.getBottom()); + } + canvas.clipRect(mTmpRect); + } + if (mSlideOffset < 1) { + drawScrim = true; + } + } + + result = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(save); + + if (drawScrim) { + final int baseAlpha = (mCoveredFadeColor & 0xff000000) >>> 24; + final int imag = (int) (baseAlpha * (1 - mSlideOffset)); + final int color = imag << 24 | (mCoveredFadeColor & 0xffffff); + mCoveredFadePaint.setColor(color); + canvas.drawRect(mTmpRect, mCoveredFadePaint); + } + + return result; + } + + /** + * Smoothly animate mDraggingPane to the target X position within its range. + * + * @param slideOffset position to animate to + * @param velocity initial velocity in case of fling, or 0. + */ + boolean smoothSlideTo(float slideOffset, int velocity) { + if (!mCanSlide) { + // Nothing to do. + return false; + } + + final int topBound = getSlidingTop(); + int y = mIsSlidingUp + ? (int) (topBound + slideOffset * mSlideRange) + : (int) (topBound - slideOffset * mSlideRange); + + if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), y)) { + setAllChildrenVisible(); + ViewCompat.postInvalidateOnAnimation(this); + return true; + } + return false; + } + + @Override + public void computeScroll() { + if (mDragHelper.continueSettling(true)) { + if (!mCanSlide) { + mDragHelper.abort(); + return; + } + + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + public void draw(Canvas c) { + super.draw(c); + + if (mSlideableView == null) { + // No need to draw a shadow if we don't have one. + return; + } + + final int right = mSlideableView.getRight(); + final int top; + final int bottom; + if (mIsSlidingUp) { + top = mSlideableView.getTop() - mShadowHeight; + bottom = mSlideableView.getTop(); + } else { + top = mSlideableView.getBottom(); + bottom = mSlideableView.getBottom() + mShadowHeight; + } + final int left = mSlideableView.getLeft(); + + if (mShadowDrawable != null) { + mShadowDrawable.setBounds(left, top, right, bottom); + mShadowDrawable.draw(c); + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + return checkV && ViewCompat.canScrollHorizontally(v, -dx); + } + + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof MarginLayoutParams + ? new LayoutParams((MarginLayoutParams) p) + : new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + ss.mSlideState = mSlideState; + + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mSlideState = ss.mSlideState; + } + + private class DragHelperCallback extends ViewDragHelper.Callback { + + @Override + public boolean tryCaptureView(View child, int pointerId) { + if (mIsUnableToDrag) { + return false; + } + + return ((LayoutParams) child.getLayoutParams()).slideable; + } + + @Override + public void onViewDragStateChanged(int state) { + int anchoredTop = (int)(mAnchorPoint*mSlideRange); + + if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) { + if (mSlideOffset == 0) { + if (mSlideState != SlideState.EXPANDED) { + updateObscuredViewVisibility(); + dispatchOnPanelExpanded(mSlideableView); + mSlideState = SlideState.EXPANDED; + } + } else if (mSlideOffset == (float)anchoredTop/(float)mSlideRange) { + if (mSlideState != SlideState.ANCHORED) { + updateObscuredViewVisibility(); + dispatchOnPanelAnchored(mSlideableView); + mSlideState = SlideState.ANCHORED; + } + } else if (mSlideState != SlideState.COLLAPSED) { + dispatchOnPanelCollapsed(mSlideableView); + mSlideState = SlideState.COLLAPSED; + } + } + } + + @Override + public void onViewCaptured(View capturedChild, int activePointerId) { + // Make all child views visible in preparation for sliding things around + setAllChildrenVisible(); + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + onPanelDragged(top); + invalidate(); + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + int top = mIsSlidingUp + ? getSlidingTop() + : getSlidingTop() - mSlideRange; + + if (mAnchorPoint != 0) { + int anchoredTop; + float anchorOffset; + if (mIsSlidingUp) { + anchoredTop = (int)(mAnchorPoint*mSlideRange); + anchorOffset = (float)anchoredTop/(float)mSlideRange; + } else { + anchoredTop = mPanelHeight - (int)(mAnchorPoint*mSlideRange); + anchorOffset = (float)(mPanelHeight - anchoredTop)/(float)mSlideRange; + } + + if (yvel > 0 || (yvel == 0 && mSlideOffset >= (1f+anchorOffset)/2)) { + top += mSlideRange; + } else if (yvel == 0 && mSlideOffset < (1f+anchorOffset)/2 + && mSlideOffset >= anchorOffset/2) { + top += mSlideRange * mAnchorPoint; + } + + } else if (yvel > 0 || (yvel == 0 && mSlideOffset > 0.5f)) { + top += mSlideRange; + } + + mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top); + invalidate(); + } + + @Override + public int getViewVerticalDragRange(View child) { + return mSlideRange; + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + final int topBound; + final int bottomBound; + if (mIsSlidingUp) { + topBound = getSlidingTop(); + bottomBound = topBound + mSlideRange; + } else { + bottomBound = getPaddingTop(); + topBound = bottomBound - mSlideRange; + } + + return Math.min(Math.max(top, topBound), bottomBound); + } + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + private static final int[] ATTRS = new int[] { + android.R.attr.layout_weight + }; + + /** + * True if this pane is the slideable pane in the layout. + */ + boolean slideable; + + /** + * True if this view should be drawn dimmed + * when it's been offset from its default position. + */ + boolean dimWhenOffset; + + Paint dimPaint; + + public LayoutParams() { + super(MATCH_PARENT, MATCH_PARENT); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(android.view.ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(LayoutParams source) { + super(source); + } + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); + a.recycle(); + } + + } + + static class SavedState extends BaseSavedState { + SlideState mSlideState; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + try { + mSlideState = Enum.valueOf(SlideState.class, in.readString()); + } catch (IllegalArgumentException e) { + mSlideState = SlideState.COLLAPSED; + } + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeString(mSlideState.toString()); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/src/com/android/launcher3/WallpaperChangedReceiver.java b/src/com/android/launcher3/WallpaperChangedReceiver.java index 2d5612f12..0a6a7efa5 100644 --- a/src/com/android/launcher3/WallpaperChangedReceiver.java +++ b/src/com/android/launcher3/WallpaperChangedReceiver.java @@ -20,10 +20,25 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import com.android.launcher3.stats.LauncherStats; + public class WallpaperChangedReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent data) { LauncherAppState.setApplicationContext(context.getApplicationContext()); LauncherAppState appState = LauncherAppState.getInstance(); appState.onWallpaperChanged(); + SharedPreferences prefs = context.getSharedPreferences(LauncherAppState + .getSharedPreferencesKey(), Context.MODE_PRIVATE); + boolean fromSelf = prefs.getBoolean(Launcher.LONGPRESS_CHANGE, false); + if (fromSelf) { + prefs.edit().putBoolean(Launcher.LONGPRESS_CHANGE, false).apply(); + LauncherApplication.getLauncherStats().sendWallpaperChangedEvent( + LauncherStats.ORIGIN_TREB_LONGPRESS); + } else { + LauncherApplication.getLauncherStats().sendWallpaperChangedEvent( + LauncherStats.ORIGIN_CHOOSER); + } + } } diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java index 346055566..056bdec53 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -66,7 +66,7 @@ public class WidgetPreviewLoader { private final IconCache mIconCache; private final UserManagerCompat mUserManager; private final AppWidgetManagerCompat mManager; - private final CacheDb mDb; + private CacheDb mDb; private final int mProfileBadgeMargin; private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); @@ -83,6 +83,13 @@ public class WidgetPreviewLoader { .getDimensionPixelSize(R.dimen.profile_badge_margin); } + public void recreateWidgetPreviewDb() { + if (mDb != null) { + mDb.close(); + } + mDb = new CacheDb(mContext); + } + /** * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be * called on UI thread diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 856e3b88a..4364e6b1b 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -68,6 +68,7 @@ import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource; import com.android.launcher3.accessibility.OverviewScreenAccessibilityDelegate; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.LongArrayMap; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.WallpaperUtils; @@ -77,6 +78,8 @@ import com.android.launcher3.widget.PendingAddWidgetInfo; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** @@ -217,6 +220,7 @@ public class Workspace extends PagedView private boolean mWorkspaceFadeInAdjacentScreens; WallpaperOffsetInterpolator mWallpaperOffset; + private boolean mScrollWallpaper; @Thunk boolean mWallpaperIsLiveWallpaper; @Thunk int mNumPagesForWallpaperParallax; @Thunk float mLastSetWallpaperOffsetSteps = 0; @@ -290,6 +294,8 @@ public class Workspace extends PagedView } }; + private boolean mHideIconLabels; + /** * Used to inflate the Workspace from XML. * @@ -312,6 +318,10 @@ public class Workspace extends PagedView mOutlineHelper = HolographicOutlineHelper.obtain(context); + mHideIconLabels = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + mLauncher = (Launcher) context; mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this); final Resources res = getResources(); @@ -351,6 +361,13 @@ public class Workspace extends PagedView } } + /** + * @return A {@link List} of {@link Long}s representing ids of the workspace screens + */ + public List<Long> getWorkspaceScreenIds() { + return mScreenOrder; + } + // estimate the size of a widget with spans hSpan, vSpan. return MAX_VALUE for each // dimension if unsuccessful public int[] estimateItemSize(ItemInfo itemInfo, boolean springLoaded) { @@ -566,6 +583,11 @@ public class Workspace extends PagedView mScreenOrder.add(insertIndex, screenId); addView(newScreen, insertIndex); + if (getDefaultScreenId() == screenId) { + int defaultPage = getPageIndexForScreenId(screenId); + moveToScreen(defaultPage, false); + } + LauncherAccessibilityDelegate delegate = LauncherAppState.getInstance().getAccessibilityDelegate(); if (delegate != null && delegate.isInAccessibleDrag()) { @@ -964,6 +986,8 @@ public class Workspace extends PagedView */ void addInScreen(View child, long container, long screenId, int x, int y, int spanX, int spanY, boolean insert, boolean computeXYFromRank) { + reloadSettings(); + if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { if (getScreenWithId(screenId) == null) { Log.e(TAG, "Skipping child, screenId " + screenId + " not found"); @@ -994,9 +1018,10 @@ public class Workspace extends PagedView screenId = mLauncher.getHotseat().getOrderInHotseat(x, y); } } else { - // Show folder title if not in the hotseat if (child instanceof FolderIcon) { - ((FolderIcon) child).setTextVisible(true); + ((FolderIcon) child).setTextVisible(!mHideIconLabels); + } else if (child instanceof BubbleTextView) { + ((BubbleTextView) child).setTextVisibility(!mHideIconLabels); } layout = getScreenWithId(screenId); child.setOnKeyListener(new IconKeyEventListener()); @@ -1362,6 +1387,7 @@ public class Workspace extends PagedView // Don't use all the wallpaper for parallax until you have at least this many pages private final int MIN_PARALLAX_PAGE_SPAN = 3; int mNumScreens; + boolean mCompletedInitialOffset; public WallpaperOffsetInterpolator() { mChoreographer = Choreographer.getInstance(); @@ -1376,7 +1402,8 @@ public class Workspace extends PagedView private void updateOffset(boolean force) { if (mWaitingForUpdate || force) { mWaitingForUpdate = false; - if (computeScrollOffset() && mWindowToken != null) { + if ((!mCompletedInitialOffset || computeScrollOffset()) && mWindowToken != null) { + mCompletedInitialOffset = true; try { mWallpaperManager.setWallpaperOffsets(mWindowToken, mWallpaperOffset.getCurrX(), 0.5f); @@ -1533,7 +1560,12 @@ public class Workspace extends PagedView @Override public void computeScroll() { super.computeScroll(); - mWallpaperOffset.syncWithScroll(); + + if (mScrollWallpaper) mWallpaperOffset.syncWithScroll(); + + if (isInOverviewMode() && !isReordering(true)) { + mLauncher.updateDefaultScreenButton(); + } } @Override @@ -1702,6 +1734,8 @@ public class Workspace extends PagedView } } + setScrollingWallpaper(); + // Update wallpaper dimensions if they were changed since last onResume // (we also always set the wallpaper dimensions in the constructor) if (LauncherAppState.getInstance().hasWallpaperChangedSinceLastCheck()) { @@ -1715,7 +1749,8 @@ public class Workspace extends PagedView @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount() + && mScrollWallpaper) { mWallpaperOffset.syncWithScroll(); mWallpaperOffset.jumpToFinal(); } @@ -1970,12 +2005,33 @@ public class Workspace extends PagedView // Re-enable auto layout transitions for page deletion. enableLayoutTransitions(); + + // Show the default screen button + mLauncher.updateDefaultScreenButton(); } public boolean isInOverviewMode() { return mState == State.OVERVIEW; } + public boolean enterOverviewMode() { + if (mTouchState != TOUCH_STATE_REST) { + return false; + } + enableOverviewMode(); + return true; + } + + public void exitOverviewMode() { + ((OverviewPanel) mLauncher.getOverviewPanel()).collapsePane(); + reloadSettings(); + } + + private void enableOverviewMode() { + reloadSettings(); + } + + int getOverviewModeTranslationY() { DeviceProfile grid = mLauncher.getDeviceProfile(); Rect workspacePadding = grid.getWorkspacePadding(Utilities.isRtl(getResources())); @@ -3523,6 +3579,7 @@ public class Workspace extends PagedView case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout, (FolderInfo) info, mIconCache); + ((FolderIcon) view).setTextVisible(!mHideIconLabels); break; default: throw new IllegalStateException("Unknown item type: " + info.itemType); @@ -3761,6 +3818,11 @@ public class Workspace extends PagedView return; } + // Drop is finished, so we should use our actual cell coordinates now. + if (mDragInfo != null) { + ((CellLayout.LayoutParams) mDragInfo.cell.getLayoutParams()).useTmpCoords = false; + } + boolean beingCalledAfterUninstall = mDeferredAction != null; if (success && !(beingCalledAfterUninstall && !mUninstallSuccessful)) { @@ -4420,7 +4482,7 @@ public class Workspace extends PagedView } void moveToDefaultScreen(boolean animate) { - moveToScreen(mDefaultPage, animate); + moveToScreen(getPageIndexForScreenId(getDefaultScreenId()), animate); } void moveToCustomContentScreen(boolean animate) { @@ -4482,6 +4544,34 @@ public class Workspace extends PagedView sourceData.putInt(Stats.SOURCE_EXTRA_CONTAINER_PAGE, getCurrentPage()); } + private void reloadSettings() { + mHideIconLabels = SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + + setScrollingWallpaper(); + } + + /** + * Gets the preference for whether to apply scrolling wallpaper effect or not and applies the + * preference. + */ + private void setScrollingWallpaper() { + mScrollWallpaper = SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL, + R.bool.preferences_interface_homescreen_scrolling_wallpaper_scroll_default); + if (!mScrollWallpaper) { + if (mWindowToken != null) mWallpaperManager.setWallpaperOffsets(mWindowToken, 0f, 0.5f); + } else { + mWallpaperOffset.syncWithScroll(); + } + } + + private long getDefaultScreenId() { + return SettingsProvider.getLongCustomDefault(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_DEFAULT_SCREEN_ID, 1); + } + /** * Used as a workaround to ensure that the AppWidgetService receives the * PACKAGE_ADDED broadcast before updating widgets. diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index 88c6acada..8e4491180 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -35,6 +35,7 @@ import android.view.ViewGroup; import android.widget.LinearLayout; import com.android.launcher3.AppInfo; import com.android.launcher3.BaseContainerView; +import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DeviceProfile; @@ -47,6 +48,7 @@ import com.android.launcher3.LauncherTransitionable; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.Thunk; @@ -55,74 +57,6 @@ import java.nio.charset.CharsetEncoder; import java.util.ArrayList; import java.util.List; - - -/** - * A merge algorithm that merges every section indiscriminately. - */ -final class FullMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm { - - @Override - public boolean continueMerging(AlphabeticalAppsList.SectionInfo section, - AlphabeticalAppsList.SectionInfo withSection, - int sectionAppCount, int numAppsPerRow, int mergeCount) { - // Don't merge the predicted apps - if (section.firstAppItem.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) { - return false; - } - // Otherwise, merge every other section - return true; - } -} - -/** - * The logic we use to merge multiple sections. We only merge sections when their final row - * contains less than a certain number of icons, and stop at a specified max number of merges. - * In addition, we will try and not merge sections that identify apps from different scripts. - */ -final class SimpleSectionMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm { - - private int mMinAppsPerRow; - private int mMinRowsInMergedSection; - private int mMaxAllowableMerges; - private CharsetEncoder mAsciiEncoder; - - public SimpleSectionMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) { - mMinAppsPerRow = minAppsPerRow; - mMinRowsInMergedSection = minRowsInMergedSection; - mMaxAllowableMerges = maxNumMerges; - mAsciiEncoder = Charset.forName("US-ASCII").newEncoder(); - } - - @Override - public boolean continueMerging(AlphabeticalAppsList.SectionInfo section, - AlphabeticalAppsList.SectionInfo withSection, - int sectionAppCount, int numAppsPerRow, int mergeCount) { - // Don't merge the predicted apps - if (section.firstAppItem.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) { - return false; - } - - // Continue merging if the number of hanging apps on the final row is less than some - // fixed number (ragged), the merged rows has yet to exceed some minimum row count, - // and while the number of merged sections is less than some fixed number of merges - int rows = sectionAppCount / numAppsPerRow; - int cols = sectionAppCount % numAppsPerRow; - - // Ensure that we do not merge across scripts, currently we only allow for english and - // native scripts so we can test if both can just be ascii encoded - boolean isCrossScript = false; - if (section.firstAppItem != null && withSection.firstAppItem != null) { - isCrossScript = mAsciiEncoder.canEncode(section.firstAppItem.sectionName) != - mAsciiEncoder.canEncode(withSection.firstAppItem.sectionName); - } - return (0 < cols && cols < mMinAppsPerRow) && - rows < mMinRowsInMergedSection && - mergeCount < mMaxAllowableMerges && - !isCrossScript; - } -} - /** * The all apps view container. */ @@ -130,8 +64,11 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc LauncherTransitionable, View.OnTouchListener, View.OnLongClickListener, AllAppsSearchBarController.Callbacks { - private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3; - private static final int MAX_NUM_MERGES_PHONE = 2; + public static final int SECTION_STRATEGY_GRID = 1; + public static final int SECTION_STRATEGY_RAGGED = 2; + + public static final int GRID_THEME_LIGHT = 1; + public static final int GRID_THEME_DARK = 2; @Thunk Launcher mLauncher; @Thunk AlphabeticalAppsList mApps; @@ -148,6 +85,10 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc private View mSearchBarView; private SpannableStringBuilder mSearchQueryBuilder = null; + private int mSectionStrategy = SECTION_STRATEGY_RAGGED; + private int mGridTheme = GRID_THEME_DARK; + private int mLastGridTheme = -1; + private int mSectionNamesMargin; private int mNumAppsPerRow; private int mNumPredictedAppsPerRow; @@ -157,6 +98,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc // This coordinate is relative to its parent private final Point mIconLastTouchPos = new Point(); + private boolean mReloadDrawer = false; + private View.OnClickListener mSearchClickListener = new View.OnClickListener() { @Override public void onClick(View v) { @@ -178,9 +121,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc Resources res = context.getResources(); mLauncher = (Launcher) context; - mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); mApps = new AlphabeticalAppsList(context); - mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, this); + mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, + this); mApps.setAdapter(mAdapter); mLayoutManager = mAdapter.getLayoutManager(); mItemDecoration = mAdapter.getItemDecoration(); @@ -191,11 +134,33 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc Selection.setSelection(mSearchQueryBuilder, 0); } + public int getNumPredictedAppsPerRow() { + return mNumPredictedAppsPerRow; + } + + /** + * Sets the current set of predicted apps by component. + * Only usable when custom predicted apps are disabled. + */ + public void setPredictedAppComponents(List<ComponentKey> apps) { + mApps.setPredictedAppComponents(apps); + updateScrubber(); + } + /** - * Sets the current set of predicted apps. + * Sets the current set of predicted apps by info. + * Only usable when custom predicated apps are enabled. */ - public void setPredictedApps(List<ComponentKey> apps) { + public void setPredictedApps(List<AppInfo> apps) { mApps.setPredictedApps(apps); + updateScrubber(); + } + + /** + * Set whether the predicted apps row will have a customized selection of apps. + */ + public void setCustomPredictedAppsEnabled(boolean enabled) { + mApps.mCustomPredictedAppsEnabled = enabled; } /** @@ -203,6 +168,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc */ public void setApps(List<AppInfo> apps) { mApps.setApps(apps); + updateScrubber(); } /** @@ -210,6 +176,17 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc */ public void addApps(List<AppInfo> apps) { mApps.addApps(apps); + updateScrubber(); + } + + /** + * Reloads the existing apps in the list + */ + public void onReloadAppDrawer() { + mReloadDrawer = true; + List<AppInfo> apps = mApps.getApps(); + updateApps(apps); + requestLayout(); } /** @@ -217,6 +194,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc */ public void updateApps(List<AppInfo> apps) { mApps.updateApps(apps); + updateScrubber(); } /** @@ -224,12 +202,55 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc */ public void removeApps(List<AppInfo> apps) { mApps.removeApps(apps); + updateScrubber(); + } + + private void updateScrubber() { + if (useScroller() && useScrubber()) { + mScrubber.updateSections(); + } + } + + public List<AppInfo> getApps() { + return mApps.getApps(); + } + + public int getSectionStrategy() { + return mSectionStrategy; + } + + private void updateSectionStrategy() { + Context context = getContext(); + Resources res = context.getResources(); + boolean useCompactGrid = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_DRAWER_STYLE_USE_COMPACT, + R.bool.preferences_interface_drawer_compact_default); + mSectionStrategy = useCompactGrid ? SECTION_STRATEGY_GRID : SECTION_STRATEGY_RAGGED; + mSectionNamesMargin = useCompactGrid ? + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) : + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections); + mAdapter.setSectionStrategy(mSectionStrategy); + mAppsRecyclerView.setSectionStrategy(mSectionStrategy); + } + + private void updateGridTheme() { + Context context = getContext(); + boolean useDarkColor= SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_DRAWER_DARK, + R.bool.preferences_interface_drawer_dark_default); + mGridTheme = useDarkColor ? GRID_THEME_DARK : GRID_THEME_LIGHT; + mAdapter.setGridTheme(mGridTheme); + updateBackgroundAndPaddings(true); } /** * Sets the search bar that shows above the a-z list. */ public void setSearchBarController(AllAppsSearchBarController searchController) { + if (searchController == null) { + mSearchBarController = null; + return; + } if (mSearchBarController != null) { throw new RuntimeException("Expected search bar controller to only be set once"); } @@ -241,11 +262,16 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mSearchBarContainerView.addView(searchBarView); mSearchBarContainerView.setVisibility(View.VISIBLE); mSearchBarView = searchBarView; - setHasSearchBar(); + setHasSearchBar(true); updateBackgroundAndPaddings(); } + public void setSearchBarContainerViewVisibility(int visibility) { + mSearchBarContainerView.setVisibility(visibility); + updateBackgroundAndPaddings(); + } + /** * Scrolls this list view to the top. */ @@ -322,7 +348,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc if (mItemDecoration != null) { mAppsRecyclerView.addItemDecoration(mItemDecoration); } - + setScroller(); + updateGridTheme(); + updateSectionStrategy(); updateBackgroundAndPaddings(); } @@ -337,26 +365,27 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc int availableWidth = !mContentBounds.isEmpty() ? mContentBounds.width() : MeasureSpec.getSize(widthMeasureSpec); DeviceProfile grid = mLauncher.getDeviceProfile(); - grid.updateAppsViewNumCols(getResources(), availableWidth); + grid.updateAppsViewNumCols(getResources(), availableWidth, + mSectionStrategy); if (mNumAppsPerRow != grid.allAppsNumCols || mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) { mNumAppsPerRow = grid.allAppsNumCols; mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols; - // If there is a start margin to draw section names, determine how we are going to merge - // app sections - boolean mergeSectionsFully = mSectionNamesMargin == 0 || !grid.isPhone; - AlphabeticalAppsList.MergeAlgorithm mergeAlgorithm = mergeSectionsFully ? - new FullMergeAlgorithm() : - new SimpleSectionMergeAlgorithm((int) Math.ceil(mNumAppsPerRow / 2f), - MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE); - mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow); mAdapter.setNumAppsPerRow(mNumAppsPerRow); - mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeAlgorithm); + + boolean mergeSections = mSectionStrategy == SECTION_STRATEGY_GRID; + mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeSections); + + mLauncher.getRemoteFolderManager().onMeasureDrawer(mNumPredictedAppsPerRow); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mReloadDrawer) { + updateBackgroundAndPaddings(true); + mReloadDrawer = false; + } } /** @@ -369,8 +398,10 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc boolean isRtl = Utilities.isRtl(getResources()); // TODO: Use quantum_panel instead of quantum_panel_shape + int bgRes = mGridTheme == GRID_THEME_DARK ? R.drawable.quantum_panel_shape_dark : + R.drawable.quantum_panel_shape; InsetDrawable background = new InsetDrawable( - getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0, + getResources().getDrawable(bgRes), padding.left, 0, padding.right, 0); Rect bgPadding = new Rect(); background.getPadding(bgPadding); @@ -389,12 +420,23 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc // names) int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getMaxScrollbarWidth()); int topBottomPadding = mRecyclerViewTopBottomPadding; + final boolean useScrollerScrubber = useScroller() && useScrubber(); if (isRtl) { mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(), - topBottomPadding, padding.right + startInset, topBottomPadding); + topBottomPadding, padding.right + startInset, useScrollerScrubber ? + mScrubberHeight + topBottomPadding : topBottomPadding); + if (useScrollerScrubber) { + mScrubberContainerView.setPadding(padding.left + + mAppsRecyclerView.getMaxScrollbarWidth(), 0, padding.right, 0); + } } else { mAppsRecyclerView.setPadding(padding.left + startInset, topBottomPadding, - padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), topBottomPadding); + padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), useScrollerScrubber ? + mScrubberHeight + topBottomPadding : topBottomPadding); + if (useScrollerScrubber) { + mScrubberContainerView.setPadding(padding.left, 0, + padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), 0); + } } // Inset the search bar to fit its bounds above the container @@ -407,7 +449,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mSearchBarContainerView.getLayoutParams(); lp.leftMargin = searchBarBounds.left - backgroundPadding.left; lp.topMargin = searchBarBounds.top - backgroundPadding.top; - lp.rightMargin = (getMeasuredWidth() - searchBarBounds.right) - backgroundPadding.right; + lp.rightMargin = (getMeasuredWidth() - searchBarBounds.right) + - backgroundPadding.right; mSearchBarContainerView.requestLayout(); } } @@ -416,7 +459,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc public boolean dispatchKeyEvent(KeyEvent event) { // Determine if the key event was actual text, if so, focus the search bar and then dispatch // the key normally so that it can process this key event - if (!mSearchBarController.isSearchFieldFocused() && + if (mSearchBarController != null && + !mSearchBarController.isSearchFieldFocused() && event.getAction() == KeyEvent.ACTION_DOWN) { final int unicodeChar = event.getUnicodeChar(); final boolean isKeyNotWhitespace = unicodeChar > 0 && @@ -556,7 +600,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { if (toWorkspace) { // Reset the search bar and base recycler view after transitioning home - mSearchBarController.reset(); + if (hasSearchBar()) { + mSearchBarController.reset(); + } mAppsRecyclerView.reset(); } } @@ -614,6 +660,12 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc public void onSearchResult(String query, ArrayList<ComponentKey> apps) { if (apps != null) { mApps.setOrderedFilter(apps); + if (mGridTheme != GRID_THEME_LIGHT) { + mLastGridTheme = mGridTheme; + mGridTheme = GRID_THEME_LIGHT; + updateBackgroundAndPaddings(true); + mAdapter.setGridTheme(mGridTheme); + } mAdapter.setLastSearchQuery(query); mAppsRecyclerView.onSearchResultsChanged(); } @@ -623,10 +675,20 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc public void clearSearchResult() { mApps.setOrderedFilter(null); mAppsRecyclerView.onSearchResultsChanged(); - + if (mLastGridTheme != -1 && mLastGridTheme != GRID_THEME_LIGHT) { + mGridTheme = mLastGridTheme; + updateBackgroundAndPaddings(true); + mAdapter.setGridTheme(mGridTheme); + mLastGridTheme = -1; + } // Clear the search query mSearchQueryBuilder.clear(); mSearchQueryBuilder.clearSpans(); Selection.setSelection(mSearchQueryBuilder, 0); } + + @Override + protected BaseRecyclerView getRecyclerView() { + return mAppsRecyclerView; + } } diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index 1f95133d4..643182588 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -24,7 +24,6 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.net.Uri; @@ -38,10 +37,14 @@ import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.TextView; import com.android.launcher3.AppInfo; +import com.android.launcher3.BaseRecyclerViewFastScrollBar.FastScrollFocusApplicator; +import com.android.launcher3.BaseRecyclerViewFastScrollBar.FastScrollFocusable; import com.android.launcher3.BubbleTextView; import com.android.launcher3.Launcher; import com.android.launcher3.R; +import com.android.launcher3.RemoteFolderManager; import com.android.launcher3.Utilities; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.Thunk; import java.util.HashMap; @@ -68,6 +71,17 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. public static final int SEARCH_MARKET_DIVIDER_VIEW_TYPE = 4; // The message to continue to a market search when there are no filtered results public static final int SEARCH_MARKET_VIEW_TYPE = 5; + // Section header for customized predicated apps. + public static final int CUSTOM_PREDICTED_APPS_HEADER_VIEW_TYPE = 6; + // Additional spacing between predicted apps and regular apps. + public static final int CUSTOM_PREDICTED_APPS_FOOTER_VIEW_TYPE = 7; + + private boolean mIconsDimmed = false; + + private int mGridTheme; + + + private AlphabeticalAppsList.SectionInfo mFocusedSection; /** * ViewHolder for each icon. @@ -142,7 +156,7 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. public class GridItemDecoration extends RecyclerView.ItemDecoration { private static final boolean DEBUG_SECTION_MARGIN = false; - private static final boolean FADE_OUT_SECTIONS = false; + private static final boolean FADE_OUT_SECTIONS = true; private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>(); private Rect mTmpBounds = new Rect(); @@ -181,36 +195,34 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. mPredictedAppsDividerPaint); hasDrawnPredictedAppsDivider = true; - } else if (showSectionNames && shouldDrawItemSection(holder, i, items)) { - // At this point, we only draw sections for each section break; + // Only customized predicted apps will draw a section name. + if (!mApps.mCustomPredictedAppsEnabled) continue; + } + + if (showSectionNames && shouldDrawItemSection(holder, items)) { + // Draw the section name for the first visible item int viewTopOffset = (2 * child.getPaddingTop()); int pos = holder.getPosition(); AlphabeticalAppsList.AdapterItem item = items.get(pos); AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo; - - // Draw all the sections for this index String lastSectionName = item.sectionName; - for (int j = item.sectionAppIndex; j < sectionInfo.numApps; j++, pos++) { - AlphabeticalAppsList.AdapterItem nextItem = items.get(pos); - String sectionName = nextItem.sectionName; - if (nextItem.sectionInfo != sectionInfo) { - break; - } - if (j > item.sectionAppIndex && sectionName.equals(lastSectionName)) { - continue; - } + // Find the section name bounds + PointF sectionBounds = getAndCacheSectionBounds(lastSectionName); - // Find the section name bounds - PointF sectionBounds = getAndCacheSectionBounds(sectionName); + // Calculate where to draw the section + int sectionBaseline = (int) (viewTopOffset + sectionBounds.y); + int x = mIsRtl ? + parent.getWidth() - mBackgroundPadding.left - mSectionNamesMargin : + mBackgroundPadding.left; + x += (int) ((mSectionNamesMargin - sectionBounds.x) / 2f); - // Calculate where to draw the section - int sectionBaseline = (int) (viewTopOffset + sectionBounds.y); - int x = mIsRtl ? - parent.getWidth() - mBackgroundPadding.left - mSectionNamesMargin : - mBackgroundPadding.left; - x += (int) ((mSectionNamesMargin - sectionBounds.x) / 2f); - int y = child.getTop() + sectionBaseline; + int y; + boolean fixedToRow = false; + if (item.viewType == PREDICTION_ICON_VIEW_TYPE) { + y = child.getTop() - (int) mSectionTextPaint.getTextSize() / 2; + } else { + y = child.getTop() + sectionBaseline; // Determine whether this is the last row with apps in that section, if // so, then fix the section to the row allowing it to scroll past the @@ -219,7 +231,7 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. int nextRowPos = Math.min(items.size() - 1, pos + mAppsPerRow - (appIndexInSection % mAppsPerRow)); AlphabeticalAppsList.AdapterItem nextRowItem = items.get(nextRowPos); - boolean fixedToRow = !sectionName.equals(nextRowItem.sectionName); + fixedToRow = !lastSectionName.equals(nextRowItem.sectionName); if (!fixedToRow) { y = Math.max(sectionBaseline, y); } @@ -229,22 +241,21 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. if (lastSectionHeight > 0 && y <= (lastSectionTop + lastSectionHeight)) { y += lastSectionTop - y + lastSectionHeight; } + } - // Draw the section header - if (FADE_OUT_SECTIONS) { - int alpha = 255; - if (fixedToRow) { - alpha = Math.min(255, - (int) (255 * (Math.max(0, y) / (float) sectionBaseline))); - } - mSectionTextPaint.setAlpha(alpha); + // Draw the section header + if (FADE_OUT_SECTIONS) { + int alpha = 255; + if (fixedToRow) { + alpha = Math.min(255, + (int) (255 * (Math.max(0, y) / (float) sectionBaseline))); } - c.drawText(sectionName, x, y, mSectionTextPaint); - - lastSectionTop = y; - lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset); - lastSectionName = sectionName; + mSectionTextPaint.setAlpha(alpha); } + c.drawText(lastSectionName, x, y, mSectionTextPaint); + + lastSectionTop = y; + lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset); i += (sectionInfo.numApps - item.sectionAppIndex); } } @@ -304,21 +315,22 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. /** * Returns whether to draw the section for the given child. */ - private boolean shouldDrawItemSection(ViewHolder holder, int childIndex, + private boolean shouldDrawItemSection(ViewHolder holder, List<AlphabeticalAppsList.AdapterItem> items) { int pos = holder.getPosition(); AlphabeticalAppsList.AdapterItem item = items.get(pos); // Ensure it's an icon - if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) { + if (item.viewType != ICON_VIEW_TYPE && item.viewType != PREDICTION_ICON_VIEW_TYPE) { return false; } - // Draw the section header for the first item in each section - return (childIndex == 0) || - (items.get(pos - 1).viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE); + + return true; } } + private final RemoteFolderManager mRemoteFolderManager; + private Launcher mLauncher; private LayoutInflater mLayoutInflater; @Thunk AlphabeticalAppsList mApps; @@ -349,9 +361,15 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. // Section drawing @Thunk int mSectionNamesMargin; @Thunk int mSectionHeaderOffset; + @Thunk int mSectionStrategy; @Thunk Paint mSectionTextPaint; @Thunk Paint mPredictedAppsDividerPaint; + private int mAllAppsTextColor; + + private int mCustomPredictedAppsHeaderHeight; + private int mCustomPredictedAppsFooterHeight; + public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnTouchListener touchListener, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) { @@ -367,13 +385,24 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. mTouchListener = touchListener; mIconClickListener = iconClickListener; mIconLongClickListener = iconLongClickListener; - mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); + mSectionNamesMargin = mSectionStrategy == + AllAppsContainerView.SECTION_STRATEGY_GRID ? + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) : + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections); + + mAllAppsTextColor = mGridTheme == AllAppsContainerView.GRID_THEME_DARK ? + res.getColor(R.color.quantum_panel_text_color_dark) : + res.getColor(R.color.quantum_panel_text_color); + mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset); mSectionTextPaint = new Paint(); mSectionTextPaint.setTextSize(res.getDimensionPixelSize( R.dimen.all_apps_grid_section_text_size)); - mSectionTextPaint.setColor(res.getColor(R.color.all_apps_grid_section_text_color)); + int sectionTextColorId = mGridTheme == AllAppsContainerView.GRID_THEME_DARK ? + R.color.all_apps_grid_section_text_color_dark : + R.color.all_apps_grid_section_text_color; + mSectionTextPaint.setColor(res.getColor(sectionTextColorId)); mSectionTextPaint.setAntiAlias(true); mPredictedAppsDividerPaint = new Paint(); @@ -381,8 +410,7 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. mPredictedAppsDividerPaint.setColor(0x1E000000); mPredictedAppsDividerPaint.setAntiAlias(true); mPredictionBarDividerOffset = - (-res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_bottom_padding) + - res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding)) / 2; + res.getDimensionPixelSize(R.dimen.all_apps_prediction_bar_divider_offset); // Resolve the market app handling additional searches PackageManager pm = launcher.getPackageManager(); @@ -391,6 +419,8 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. if (marketInfo != null) { mMarketAppName = marketInfo.loadLabel(pm).toString(); } + + mRemoteFolderManager = launcher.getRemoteFolderManager(); } /** @@ -408,6 +438,15 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. mIsRtl = rtl; } + public void setSectionStrategy(int sectionStrategy) { + Resources res = mLauncher.getResources(); + mSectionStrategy = sectionStrategy; + mSectionNamesMargin = mSectionStrategy == + AllAppsContainerView.SECTION_STRATEGY_GRID ? + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) : + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections); + } + /** * Sets the last search query that was made, used to show when there are no results and to also * seed the intent for searching the market. @@ -449,30 +488,50 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + boolean hideIconLabels = SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, + R.bool.preferences_interface_drawer_hide_icon_labels_default); switch (viewType) { case SECTION_BREAK_VIEW_TYPE: return new ViewHolder(new View(parent.getContext())); case ICON_VIEW_TYPE: { BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( R.layout.all_apps_icon, parent, false); + if (hideIconLabels) { + icon.setTextVisibility(!hideIconLabels); + } icon.setOnTouchListener(mTouchListener); icon.setOnClickListener(mIconClickListener); icon.setOnLongClickListener(mIconLongClickListener); icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) .getLongPressTimeout()); icon.setFocusable(true); + FastScrollFocusApplicator.createApplicator(icon, + FastScrollFocusable.FAST_SCROLL_FOCUS_DIMMABLE | + FastScrollFocusable.FAST_SCROLL_FOCUS_SCALABLE); + return new ViewHolder(icon); } case PREDICTION_ICON_VIEW_TYPE: { BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( R.layout.all_apps_prediction_bar_icon, parent, false); + if (hideIconLabels) { + icon.setTextVisibility(!hideIconLabels); + } icon.setOnTouchListener(mTouchListener); icon.setOnClickListener(mIconClickListener); icon.setOnLongClickListener(mIconLongClickListener); icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) .getLongPressTimeout()); icon.setFocusable(true); - return new ViewHolder(icon); + FastScrollFocusApplicator.createApplicator(icon, + FastScrollFocusable.FAST_SCROLL_FOCUS_DIMMABLE | + FastScrollFocusable.FAST_SCROLL_FOCUS_SCALABLE); + + ViewHolder holder = new ViewHolder(icon); + mRemoteFolderManager.onCreateViewHolder(holder, viewType); + + return holder; } case EMPTY_SEARCH_VIEW_TYPE: return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, @@ -490,6 +549,22 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. } }); return new ViewHolder(searchMarketView); + case CUSTOM_PREDICTED_APPS_HEADER_VIEW_TYPE: { + View v = mLayoutInflater.inflate( + R.layout.custom_predicted_apps_header, parent, false); + FastScrollFocusApplicator.createApplicator(v, + FastScrollFocusable.FAST_SCROLL_FOCUS_DIMMABLE); + ViewHolder holder = new ViewHolder(v); + mRemoteFolderManager.onCreateViewHolder(holder, viewType); + return holder; + } + case CUSTOM_PREDICTED_APPS_FOOTER_VIEW_TYPE: { + View v = mLayoutInflater.inflate(R.layout.custom_predicted_apps_footer, + parent, false); + ViewHolder holder = new ViewHolder(v); + mRemoteFolderManager.onCreateViewHolder(holder, viewType); + return holder; + } default: throw new RuntimeException("Unexpected view type"); } @@ -497,17 +572,38 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. @Override public void onBindViewHolder(ViewHolder holder, int position) { + boolean hideIconLabels = SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, + R.bool.preferences_interface_drawer_hide_icon_labels_default); + FastScrollFocusApplicator.setFastScrollDimmed(holder.mContent, false, false); + FastScrollFocusApplicator.setFastScrollFocused(holder.mContent, false, false); switch (holder.getItemViewType()) { case ICON_VIEW_TYPE: { AppInfo info = mApps.getAdapterItems().get(position).appInfo; BubbleTextView icon = (BubbleTextView) holder.mContent; + icon.setTextColor(mAllAppsTextColor); + if (hideIconLabels) { + icon.setTextVisibility(!hideIconLabels); + } icon.applyFromApplicationInfo(info); + FastScrollFocusApplicator.setFastScrollDimmed(icon, shouldDimPosition(position), + !mIconsDimmed); + FastScrollFocusApplicator.setFastScrollFocused(icon, false, !mIconsDimmed); break; } case PREDICTION_ICON_VIEW_TYPE: { AppInfo info = mApps.getAdapterItems().get(position).appInfo; BubbleTextView icon = (BubbleTextView) holder.mContent; + icon.setTextColor(mAllAppsTextColor); + if (hideIconLabels) { + icon.setTextVisibility(!hideIconLabels); + } icon.applyFromApplicationInfo(info); + FastScrollFocusApplicator.setFastScrollDimmed(icon, shouldDimPosition(position), + !mIconsDimmed); + FastScrollFocusApplicator.setFastScrollFocused(icon, false, !mIconsDimmed); + + mRemoteFolderManager.onBindViewHolder(holder, info); break; } case EMPTY_SEARCH_VIEW_TYPE: @@ -528,7 +624,42 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. searchView.setVisibility(View.GONE); } break; + case CUSTOM_PREDICTED_APPS_HEADER_VIEW_TYPE: { + TextView title = (TextView) holder.mContent.findViewById(R.id.title); + title.setTextColor(mAllAppsTextColor); + FastScrollFocusApplicator.setFastScrollDimmed(holder.mContent, + shouldDimPosition(position), !mIconsDimmed); + FastScrollFocusApplicator.setFastScrollFocused(holder.mContent, false, !mIconsDimmed); + + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) holder.mContent.getLayoutParams(); + mCustomPredictedAppsHeaderHeight = holder.mContent.getHeight() + + lp.topMargin + lp.bottomMargin; + break; + } + case CUSTOM_PREDICTED_APPS_FOOTER_VIEW_TYPE: + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) holder.mContent.getLayoutParams(); + mCustomPredictedAppsFooterHeight = holder.mContent.getHeight() + + lp.topMargin + lp.bottomMargin; + } + } + + private boolean shouldDimPosition(int position) { + if (mFocusedSection != null && mIconsDimmed) { + if (position >= mFocusedSection.firstAppItem.position && + position < mFocusedSection.firstAppItem.position + + mFocusedSection.numApps) { + return false; + } } + return mIconsDimmed; + } + + public int getCustomPredictedAppsOffset(int rowIndex) { + int offset = mCustomPredictedAppsHeaderHeight; + if (rowIndex > 0) offset += mCustomPredictedAppsFooterHeight; + return offset; } @Override @@ -542,6 +673,34 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. return item.viewType; } + public void setIconsDimmed(boolean iconsDimmed) { + if (mIconsDimmed != iconsDimmed) { + mIconsDimmed = iconsDimmed; + notifyDataSetChanged(); + } + } + + public void setFocusedSection( + AlphabeticalAppsList.SectionInfo focusedSection) { + mFocusedSection = focusedSection; + } + + public void setGridTheme(int gridTheme) { + mGridTheme = gridTheme; + int sectionTextColorId = mGridTheme == AllAppsContainerView.GRID_THEME_DARK ? + R.color.all_apps_grid_section_text_color_dark : + R.color.all_apps_grid_section_text_color; + mSectionTextPaint.setColor(mLauncher.getColor(sectionTextColorId)); + + mAllAppsTextColor = mGridTheme == AllAppsContainerView.GRID_THEME_DARK ? + mLauncher.getColor(R.color.quantum_panel_text_color_dark) : + mLauncher.getColor(R.color.quantum_panel_text_color); + + int mPredictedAppsDividerColorId = mGridTheme == AllAppsContainerView.GRID_THEME_DARK ? + R.color.drawer_divider_dark : R.color.drawer_divider_light; + mPredictedAppsDividerPaint.setColor(mLauncher.getColor(mPredictedAppsDividerColorId)); + } + /** * Creates a new market search intent. */ diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index 2f66e2cad..63acba5b2 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -15,7 +15,6 @@ */ package com.android.launcher3.allapps; -import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; @@ -27,13 +26,14 @@ import android.util.AttributeSet; import android.view.View; import com.android.launcher3.BaseRecyclerView; -import com.android.launcher3.BaseRecyclerViewFastScrollBar; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.BaseRecyclerViewFastScrollBar.FastScrollFocusApplicator; import com.android.launcher3.R; import com.android.launcher3.Stats; import com.android.launcher3.Utilities; import com.android.launcher3.util.Thunk; +import java.util.ArrayList; import java.util.List; /** @@ -50,9 +50,11 @@ public class AllAppsRecyclerView extends BaseRecyclerView private AlphabeticalAppsList mApps; private int mNumAppsPerRow; + private int mSectionStrategy = AllAppsContainerView.SECTION_STRATEGY_RAGGED; - @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView; + @Thunk ArrayList<View> mLastFastScrollFocusedViews = new ArrayList(); @Thunk int mPrevFastScrollFocusedPosition; + @Thunk AlphabeticalAppsList.SectionInfo mPrevFastScrollFocusedSection; @Thunk int mFastScrollFrameIndex; @Thunk final int[] mFastScrollFrames = new int[10]; @@ -60,6 +62,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW; private ScrollPositionState mScrollPosState = new ScrollPositionState(); + private boolean mFastScrollDragging; private AllAppsBackgroundDrawable mEmptySearchBackground; private int mEmptySearchBackgroundTopOffset; @@ -81,9 +84,30 @@ public class AllAppsRecyclerView extends BaseRecyclerView super(context, attrs, defStyleAttr); Resources res = getResources(); - mScrollbar.setDetachThumbOnFastScroll(); + if (mUseScrollbar) { + mScrollbar.setDetachThumbOnFastScroll(); + } mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize( R.dimen.all_apps_empty_search_bg_top_offset); + + addOnScrollListener(new FocusScrollListener()); + } + + private class FocusScrollListener extends OnScrollListener { + public FocusScrollListener() { } + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + switch (newState) { + case SCROLL_STATE_IDLE: + // Don't change anything if we've stopped touching the scroll bar. + if (mFastScrollDragging) { + // Animation completed, set the fast scroll state on the target views + setSectionFastScrollFocused(mPrevFastScrollFocusedPosition); + setSectionFastScrollDimmed(mPrevFastScrollFocusedPosition, false, true); + } + } + } } /** @@ -107,15 +131,23 @@ public class AllAppsRecyclerView extends BaseRecyclerView pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow); pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow); pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows); + pool.setMaxRecycledViews(AllAppsGridAdapter.CUSTOM_PREDICTED_APPS_HEADER_VIEW_TYPE, 1); + pool.setMaxRecycledViews(AllAppsGridAdapter.CUSTOM_PREDICTED_APPS_FOOTER_VIEW_TYPE, 1); + } + + public void setSectionStrategy(int sectionStrategy) { + mSectionStrategy = sectionStrategy; } /** * Scrolls this recycler view to the top. */ public void scrollToTop() { - // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling - if (mScrollbar.isThumbDetached()) { - mScrollbar.reattachThumbToScroll(); + if (mUseScrollbar) { + // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling + if (mScrollbar.isThumbDetached()) { + mScrollbar.reattachThumbToScroll(); + } } scrollToPosition(0); } @@ -226,36 +258,30 @@ public class AllAppsRecyclerView extends BaseRecyclerView throw new RuntimeException("Unexpected scroll bar mode"); } - // Map the touch position back to the scroll of the recycler view - getCurScrollState(mScrollPosState); - int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); - LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); - if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { - layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); + // Reset the last focused section + if (mPrevFastScrollFocusedSection != lastInfo.sectionInfo) { + setSectionFastScrollDimmed(mPrevFastScrollFocusedPosition, true, true); + clearSectionFocusedItems(); } - if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) { - mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position; + mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position; + mPrevFastScrollFocusedSection = lastInfo.sectionInfo; - // Reset the last focused view - if (mLastFastScrollFocusedView != null) { - mLastFastScrollFocusedView.setFastScrollFocused(false, true); - mLastFastScrollFocusedView = null; - } + getCurScrollState(mScrollPosState); + if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) { + smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState); + + setSectionFastScrollDimmed(mPrevFastScrollFocusedPosition, false, true); + setSectionFastScrollFocused(mPrevFastScrollFocusedPosition); + } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { + // Map the touch position back to the scroll of the recycler view + int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); + LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); + layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); - if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) { - smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState); - } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { - final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); - if (vh != null && - vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { - mLastFastScrollFocusedView = - (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; - mLastFastScrollFocusedView.setFastScrollFocused(true, true); - } - } else { - throw new RuntimeException("Unexpected fast scroll mode"); - } + setSectionFastScrollFocused(mPrevFastScrollFocusedPosition); + } else { + throw new RuntimeException("Unexpected fast scroll mode"); } return lastInfo.sectionName; } @@ -263,12 +289,11 @@ public class AllAppsRecyclerView extends BaseRecyclerView @Override public void onFastScrollCompleted() { super.onFastScrollCompleted(); - // Reset and clean up the last focused view - if (mLastFastScrollFocusedView != null) { - mLastFastScrollFocusedView.setFastScrollFocused(false, true); - mLastFastScrollFocusedView = null; - } + + // Reset and clean up the last focused views + clearSectionFocusedItems(); mPrevFastScrollFocusedPosition = -1; + mPrevFastScrollFocusedSection = null; } /** @@ -276,6 +301,9 @@ public class AllAppsRecyclerView extends BaseRecyclerView */ @Override public void onUpdateScrollbar(int dy) { + if (!mUseScrollbar) { + return; + } List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); // Skip early if there are no items or we haven't been measured @@ -294,7 +322,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView // Only show the scrollbar if there is height to be scrolled int availableScrollBarHeight = getAvailableScrollBarHeight(); - int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight); + int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), + mScrollPosState.rowHeight); if (availableScrollHeight <= 0) { mScrollbar.setThumbOffset(-1, -1); return; @@ -303,8 +332,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView // Calculate the current scroll position, the scrollY of the recycler view accounts for the // view padding, while the scrollBarY is drawn right up to the background padding (ignoring // padding) - int scrollY = getPaddingTop() + - (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset; + int scrollY = getCurrentScroll(mScrollPosState); int scrollBarY = mBackgroundPadding.top + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); @@ -354,6 +382,88 @@ public class AllAppsRecyclerView extends BaseRecyclerView } } + @Override + public String scrollToSection(String sectionName) { + List<AlphabeticalAppsList.FastScrollSectionInfo> scrollSectionInfos = + mApps.getFastScrollerSections(); + if (scrollSectionInfos != null) { + for (int i = 0; i < scrollSectionInfos.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo info = scrollSectionInfos.get(i); + if (info.sectionName.equals(sectionName)) { + scrollToPositionAtProgress(info.touchFraction); + return info.sectionName; + } + } + } + return null; + } + + @Override + public String[] getSectionNames() { + List<AlphabeticalAppsList.FastScrollSectionInfo> scrollSectionInfos = + mApps.getFastScrollerSections(); + if (scrollSectionInfos != null) { + String[] sectionNames = new String[scrollSectionInfos.size()]; + for (int i = 0; i < scrollSectionInfos.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo info = scrollSectionInfos.get(i); + sectionNames[i] = info.sectionName; + } + + return sectionNames; + } + return new String[0]; + } + + private void setSectionFastScrollFocused(int position) { + if (mPrevFastScrollFocusedSection != null) { + ((AllAppsGridAdapter)getAdapter()).setFocusedSection(mPrevFastScrollFocusedSection); + int size = mPrevFastScrollFocusedSection.numApps + + mPrevFastScrollFocusedSection.numOtherViews; + for (int i = 0; i < size; i++) { + int sectionPosition = position+i; + final ViewHolder vh = findViewHolderForAdapterPosition(sectionPosition); + if (vh != null) { + FastScrollFocusApplicator.setFastScrollFocused(vh.itemView, true, true); + mLastFastScrollFocusedViews.add(vh.itemView); + } + } + } + } + + @Override + public void setPreviousSectionFastScrollFocused() { + setSectionFastScrollFocused(mPrevFastScrollFocusedPosition); + } + + private void setSectionFastScrollDimmed(int position, boolean dimmed, boolean animate) { + if (mPrevFastScrollFocusedSection != null) { + int size = mPrevFastScrollFocusedSection.numApps + + mPrevFastScrollFocusedSection.numOtherViews; + for (int i = 0; i < size; i++) { + int sectionPosition = position+i; + final ViewHolder vh = findViewHolderForAdapterPosition(sectionPosition); + if (vh != null) { + FastScrollFocusApplicator.setFastScrollDimmed(vh.itemView, dimmed, animate); + } + } + } + } + + private void clearSectionFocusedItems() { + final int N = mLastFastScrollFocusedViews.size(); + for (int i = 0; i < N; i++) { + View view = mLastFastScrollFocusedViews.get(i); + FastScrollFocusApplicator.setFastScrollFocused(view, false, true); + } + mLastFastScrollFocusedViews.clear(); + } + + @Override + public void setFastScrollDragging(boolean dragging) { + ((AllAppsGridAdapter) getAdapter()).setIconsDimmed(dragging); + mFastScrollDragging = dragging; + } + /** * This runnable runs a single frame of the smooth scroll animation and posts the next frame * if necessary. @@ -362,19 +472,13 @@ public class AllAppsRecyclerView extends BaseRecyclerView @Override public void run() { if (mFastScrollFrameIndex < mFastScrollFrames.length) { + setSectionFastScrollDimmed(mPrevFastScrollFocusedPosition, false, true); scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]); mFastScrollFrameIndex++; postOnAnimation(mSmoothSnapNextFrameRunnable); } else { - // Animation completed, set the fast scroll state on the target view - final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); - if (vh != null && - vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView && - mLastFastScrollFocusedView != vh.itemView) { - mLastFastScrollFocusedView = - (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; - mLastFastScrollFocusedView.setFastScrollFocused(true, true); - } + setSectionFastScrollDimmed(mPrevFastScrollFocusedPosition, false, false); + setSectionFastScrollFocused(mPrevFastScrollFocusedPosition); } } }; @@ -388,8 +492,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView // Calculate the full animation from the current scroll position to the final scroll // position, and then run the animation for the duration. - int curScrollY = getPaddingTop() + - (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; + int curScrollY = getCurrentScroll(scrollPosState); int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight); int numFrames = mFastScrollFrames.length; for (int i = 0; i < numFrames; i++) { @@ -423,7 +526,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { stateOut.rowIndex = item.rowIndex; - stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); + stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child) - + getAdditionalScrollHeight(stateOut.rowIndex); stateOut.rowHeight = child.getHeight(); break; } @@ -431,6 +535,16 @@ public class AllAppsRecyclerView extends BaseRecyclerView } } + @Override + protected int getAvailableScrollHeight(int rowCount, int rowHeight) { + return super.getAvailableScrollHeight(rowCount, rowHeight) + + getAdditionalScrollHeight(mApps.getAdapterItems().size()); + } + + private int getAdditionalScrollHeight(int rowIndex) { + return ((AllAppsGridAdapter) getAdapter()).getCustomPredictedAppsOffset(rowIndex); + } + /** * Returns the scrollY for the given position in the adapter. */ @@ -439,6 +553,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { int offset = item.rowIndex > 0 ? getPaddingTop() : 0; + offset += ((AllAppsGridAdapter) getAdapter()). + getCustomPredictedAppsOffset(item.rowIndex); return offset + item.rowIndex * rowHeight; } else { return 0; diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java index 14e2a1863..d5ebdab07 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java @@ -17,9 +17,7 @@ package com.android.launcher3.allapps; import android.content.Context; import android.graphics.Bitmap; -import android.graphics.Canvas; import android.util.AttributeSet; -import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import com.android.launcher3.BubbleTextView; diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index dac0df12a..9d30f81ee 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -21,14 +21,17 @@ import android.util.Log; import com.android.launcher3.AppInfo; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.ProtectedComponentsHelper; import com.android.launcher3.compat.AlphabeticIndexCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.model.AppNameComparator; import com.android.launcher3.util.ComponentKey; +import cyanogenmod.providers.CMSettings; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -46,6 +49,9 @@ public class AlphabeticalAppsList { private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0; private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1; + private static final String CUSTOM_PREDICTIONS_SCRUBBER = "★"; + private static final String CUSTOM_PREDICTIONS_HEADER = "☆"; + private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS; /** @@ -54,6 +60,8 @@ public class AlphabeticalAppsList { public static class SectionInfo { // The number of applications in this section public int numApps; + // The number of drawn (non-app) adapter items in this section. + public int numOtherViews; // The section break AdapterItem for this section public AdapterItem sectionBreakItem; // The first app AdapterItem for this section @@ -61,19 +69,21 @@ public class AlphabeticalAppsList { } /** - * Info about a fast scroller section, depending if sections are merged, the fast scroller - * sections will not be the same set as the section headers. + * Info about a fast scroller section. */ public static class FastScrollSectionInfo { // The section name public String sectionName; + // Info for this section + public SectionInfo sectionInfo; // The AdapterItem to scroll to for this section public AdapterItem fastScrollToItem; // The touch fraction that should map to this fast scroll section info public float touchFraction; - public FastScrollSectionInfo(String sectionName) { + public FastScrollSectionInfo(String sectionName, SectionInfo sectionInfo) { this.sectionName = sectionName; + this.sectionInfo = sectionInfo; } } @@ -155,14 +165,20 @@ public class AlphabeticalAppsList { item.position = pos; return item; } - } - /** - * Common interface for different merging strategies. - */ - public interface MergeAlgorithm { - boolean continueMerging(SectionInfo section, SectionInfo withSection, - int sectionAppCount, int numAppsPerRow, int mergeCount); + public static AdapterItem asCustomPredictedAppsHeader(int pos) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.CUSTOM_PREDICTED_APPS_HEADER_VIEW_TYPE; + item.position = pos; + return item; + } + + public static AdapterItem asPredictedAppsSpacer(int pos) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.CUSTOM_PREDICTED_APPS_FOOTER_VIEW_TYPE; + item.position = pos; + return item; + } } private Launcher mLauncher; @@ -177,7 +193,7 @@ public class AlphabeticalAppsList { private List<AdapterItem> mAdapterItems = new ArrayList<>(); // The set of sections for the apps with the current filter private List<SectionInfo> mSections = new ArrayList<>(); - // The set of sections that we allow fast-scrolling to (includes non-merged sections) + // The set of sections that we allow fast-scrolling to private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); // The set of predicted app component names private List<ComponentKey> mPredictedAppComponents = new ArrayList<>(); @@ -189,11 +205,13 @@ public class AlphabeticalAppsList { private RecyclerView.Adapter mAdapter; private AlphabeticIndexCompat mIndexer; private AppNameComparator mAppNameComparator; - private MergeAlgorithm mMergeAlgorithm; + private boolean mMergeSections; private int mNumAppsPerRow; private int mNumPredictedAppsPerRow; private int mNumAppRowsInAdapter; + boolean mCustomPredictedAppsEnabled; + public AlphabeticalAppsList(Context context) { mLauncher = (Launcher) context; mIndexer = new AlphabeticIndexCompat(context); @@ -204,10 +222,10 @@ public class AlphabeticalAppsList { * Sets the number of apps per row. */ public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow, - MergeAlgorithm mergeAlgorithm) { + boolean mergeSections) { mNumAppsPerRow = numAppsPerRow; mNumPredictedAppsPerRow = numPredictedAppsPerRow; - mMergeAlgorithm = mergeAlgorithm; + mMergeSections = mergeSections; updateAdapterItems(); } @@ -289,13 +307,33 @@ public class AlphabeticalAppsList { * Sets the current set of predicted apps. Since this can be called before we get the full set * of applications, we should merge the results only in onAppsUpdated() which is idempotent. */ - public void setPredictedApps(List<ComponentKey> apps) { + public void setPredictedAppComponents(List<ComponentKey> apps) { + if (!mCustomPredictedAppsEnabled) { + throw new IllegalStateException("Unable to set predicted app components when adapter " + + "is set to accept a custom predicted apps list."); + } + mPredictedAppComponents.clear(); mPredictedAppComponents.addAll(apps); onAppsUpdated(); } /** + * Sets the current set of predicted apps. This uses the info directly, so we do not + * merge data in {@link #onAppsUpdated()}, but go directly to {@link #updateAdapterItems()}. + */ + public void setPredictedApps(List<AppInfo> apps) { + if (!mCustomPredictedAppsEnabled) { + throw new IllegalStateException("Unable to set predicted apps directly when adapter " + + "is not set to accept a custom predicted apps list."); + } + + mPredictedApps.clear(); + mPredictedApps.addAll(apps); + updateAdapterItems(); + } + + /** * Sets the current set of apps. */ public void setApps(List<AppInfo> apps) { @@ -412,27 +450,48 @@ public class AlphabeticalAppsList { } // Process the predicted app components - mPredictedApps.clear(); - if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) { - for (ComponentKey ck : mPredictedAppComponents) { - AppInfo info = mComponentToAppMap.get(ck); - if (info != null) { - mPredictedApps.add(info); - } else { - if (LauncherAppState.isDogfoodBuild()) { - Log.e(TAG, "Predicted app not found: " + ck.flattenToString(mLauncher)); + boolean hasPredictedApps; + + // We haven't measured yet. Skip this for now. We will set properly after measure. + if (mNumPredictedAppsPerRow == 0) { + hasPredictedApps = false; + } else if (mCustomPredictedAppsEnabled) { + hasPredictedApps = !mPredictedApps.isEmpty(); + } else { + mPredictedApps.clear(); + hasPredictedApps = mPredictedAppComponents != null && + !mPredictedAppComponents.isEmpty(); + } + + if (hasPredictedApps && !hasFilter()) { + if (!mCustomPredictedAppsEnabled) { + for (ComponentKey ck : mPredictedAppComponents) { + AppInfo info = mComponentToAppMap.get(ck); + if (info != null) { + mPredictedApps.add(info); + } else { + if (LauncherAppState.isDogfoodBuild()) { + Log.e(TAG, "Predicted app not found: " + ck.flattenToString(mLauncher)); + } + } + // Stop at the number of predicted apps + if (mPredictedApps.size() == mNumPredictedAppsPerRow) { + break; } } - // Stop at the number of predicted apps - if (mPredictedApps.size() == mNumPredictedAppsPerRow) { - break; + } else { + // Shrink to column count. + if (mPredictedApps.size() > mNumPredictedAppsPerRow) { + mPredictedApps.subList(mNumAppsPerRow, mPredictedApps.size()).clear(); } } if (!mPredictedApps.isEmpty()) { // Add a section for the predictions lastSectionInfo = new SectionInfo(); - lastFastScrollerSectionInfo = new FastScrollSectionInfo(""); + String text = mCustomPredictedAppsEnabled ? CUSTOM_PREDICTIONS_SCRUBBER : " "; + lastFastScrollerSectionInfo = + new FastScrollSectionInfo(text, lastSectionInfo); AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); mSections.add(lastSectionInfo); mFastScrollerSections.add(lastFastScrollerSectionInfo); @@ -440,8 +499,9 @@ public class AlphabeticalAppsList { // Add the predicted app items for (AppInfo info : mPredictedApps) { + text = mCustomPredictedAppsEnabled ? CUSTOM_PREDICTIONS_HEADER : " "; AdapterItem appItem = AdapterItem.asPredictedApp(position++, lastSectionInfo, - "", lastSectionInfo.numApps++, info, appIndex++); + text, lastSectionInfo.numApps++, info, appIndex++); if (lastSectionInfo.firstAppItem == null) { lastSectionInfo.firstAppItem = appItem; lastFastScrollerSectionInfo.fastScrollToItem = appItem; @@ -449,19 +509,30 @@ public class AlphabeticalAppsList { mAdapterItems.add(appItem); mFilteredApps.add(info); } + + if (mCustomPredictedAppsEnabled) { + position = mLauncher.getRemoteFolderManager().onUpdateAdapterItems( + mAdapterItems, lastFastScrollerSectionInfo, lastSectionInfo, position); + } } } + ProtectedComponentsHelper.updateProtectedComponentsLists(mLauncher); + // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the // ordered set of sections for (AppInfo info : getFiltersAppInfos()) { + if (ProtectedComponentsHelper.isProtectedApp(info.flags, info.componentName)) { + continue; + } + String sectionName = getAndUpdateCachedSectionName(info.title); // Create a new section if the section names do not match if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) { lastSectionName = sectionName; lastSectionInfo = new SectionInfo(); - lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); + lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName, lastSectionInfo); mSections.add(lastSectionInfo); mFastScrollerSections.add(lastFastScrollerSectionInfo); @@ -493,7 +564,7 @@ public class AlphabeticalAppsList { mAdapterItems.add(AdapterItem.asMarketSearch(position++)); } - // Merge multiple sections together as requested by the merge strategy for this device + // Merge multiple sections together as needed. mergeSections(); if (mNumAppsPerRow != 0) { @@ -527,7 +598,8 @@ public class AlphabeticalAppsList { for (FastScrollSectionInfo info : mFastScrollerSections) { AdapterItem item = info.fastScrollToItem; if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && - item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE && + item.viewType != AllAppsGridAdapter.CUSTOM_PREDICTED_APPS_HEADER_VIEW_TYPE) { info.touchFraction = 0f; continue; } @@ -542,7 +614,8 @@ public class AlphabeticalAppsList { for (FastScrollSectionInfo info : mFastScrollerSections) { AdapterItem item = info.fastScrollToItem; if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && - item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE && + item.viewType != AllAppsGridAdapter.CUSTOM_PREDICTED_APPS_HEADER_VIEW_TYPE) { info.touchFraction = 0f; continue; } @@ -579,52 +652,18 @@ public class AlphabeticalAppsList { */ private void mergeSections() { // Ignore merging until we have an algorithm and a valid row size - if (mMergeAlgorithm == null || mNumAppsPerRow == 0) { + if (!mMergeSections || mNumAppsPerRow == 0 || hasFilter()) { return; } - // Go through each section and try and merge some of the sections - if (!hasFilter()) { - int sectionAppCount = 0; - for (int i = 0; i < mSections.size() - 1; i++) { - SectionInfo section = mSections.get(i); - sectionAppCount = section.numApps; - int mergeCount = 1; - - // Merge rows based on the current strategy - while (i < (mSections.size() - 1) && - mMergeAlgorithm.continueMerging(section, mSections.get(i + 1), - sectionAppCount, mNumAppsPerRow, mergeCount)) { - SectionInfo nextSection = mSections.remove(i + 1); - - // Remove the next section break - mAdapterItems.remove(nextSection.sectionBreakItem); - int pos = mAdapterItems.indexOf(section.firstAppItem); - - // Point the section for these new apps to the merged section - int nextPos = pos + section.numApps; - for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) { - AdapterItem item = mAdapterItems.get(j); - item.sectionInfo = section; - item.sectionAppIndex += section.numApps; - } - - // Update the following adapter items of the removed section item - pos = mAdapterItems.indexOf(nextSection.firstAppItem); - for (int j = pos; j < mAdapterItems.size(); j++) { - AdapterItem item = mAdapterItems.get(j); - item.position--; - } - section.numApps += nextSection.numApps; - sectionAppCount += nextSection.numApps; - - if (DEBUG) { - Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName + - " to " + section.firstAppItem.sectionName + - " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow)); - } - mergeCount++; - } + Iterator<AdapterItem> iter = mAdapterItems.iterator(); + int positionShift = 0; + while (iter.hasNext()) { + AdapterItem item = iter.next(); + item.position -= positionShift; + if (item.viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE) { + iter.remove(); + positionShift++; } } } diff --git a/src/com/android/launcher3/list/AutoScrollListView.java b/src/com/android/launcher3/list/AutoScrollListView.java new file mode 100644 index 000000000..733a753c1 --- /dev/null +++ b/src/com/android/launcher3/list/AutoScrollListView.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2010 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.list; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ListView; + +/** + * A ListView that can be asked to scroll (smoothly or otherwise) to a specific + * position. This class takes advantage of similar functionality that exists + * in {@link ListView} and enhances it. + */ +public class AutoScrollListView extends ListView { + + /** + * Position the element at about 1/3 of the list height + */ + private static final float PREFERRED_SELECTION_OFFSET_FROM_TOP = 0.33f; + + private int mRequestedScrollPosition = -1; + private boolean mSmoothScrollRequested; + + public AutoScrollListView(Context context) { + super(context); + } + + public AutoScrollListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Brings the specified position to view by optionally performing a jump-scroll maneuver: + * first it jumps to some position near the one requested and then does a smooth + * scroll to the requested position. This creates an impression of full smooth + * scrolling without actually traversing the entire list. If smooth scrolling is + * not requested, instantly positions the requested item at a preferred offset. + */ + public void requestPositionToScreen(int position, boolean smoothScroll) { + mRequestedScrollPosition = position; + mSmoothScrollRequested = smoothScroll; + requestLayout(); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + if (mRequestedScrollPosition == -1) { + return; + } + + final int position = mRequestedScrollPosition; + mRequestedScrollPosition = -1; + + int firstPosition = getFirstVisiblePosition() + 1; + int lastPosition = getLastVisiblePosition(); + if (position >= firstPosition && position <= lastPosition) { + return; // Already on screen + } + + final int offset = (int) (getHeight() * PREFERRED_SELECTION_OFFSET_FROM_TOP); + if (!mSmoothScrollRequested) { + setSelectionFromTop(position, offset); + + // Since we have changed the scrolling position, we need to redo child layout + // Calling "requestLayout" in the middle of a layout pass has no effect, + // so we call layoutChildren explicitly + super.layoutChildren(); + + } else { + // We will first position the list a couple of screens before or after + // the new selection and then scroll smoothly to it. + int twoScreens = (lastPosition - firstPosition) * 2; + int preliminaryPosition; + if (position < firstPosition) { + preliminaryPosition = position + twoScreens; + if (preliminaryPosition >= getCount()) { + preliminaryPosition = getCount() - 1; + } + if (preliminaryPosition < firstPosition) { + setSelection(preliminaryPosition); + super.layoutChildren(); + } + } else { + preliminaryPosition = position - twoScreens; + if (preliminaryPosition < 0) { + preliminaryPosition = 0; + } + if (preliminaryPosition > lastPosition) { + setSelection(preliminaryPosition); + super.layoutChildren(); + } + } + + + smoothScrollToPositionFromTop(position, offset); + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/list/CompositeCursorAdapter.java b/src/com/android/launcher3/list/CompositeCursorAdapter.java new file mode 100644 index 000000000..b163c501c --- /dev/null +++ b/src/com/android/launcher3/list/CompositeCursorAdapter.java @@ -0,0 +1,532 @@ +/* + * Copyright (C) 2010 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.list; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +import java.util.ArrayList; + +/** + * A general purpose adapter that is composed of multiple cursors. It just + * appends them in the order they are added. + */ +public abstract class CompositeCursorAdapter extends BaseAdapter { + + private static final int INITIAL_CAPACITY = 2; + + public static class Partition { + boolean showIfEmpty; + boolean hasHeader; + + Cursor cursor; + int idColumnIndex; + int count; + + public Partition(boolean showIfEmpty, boolean hasHeader) { + this.showIfEmpty = showIfEmpty; + this.hasHeader = hasHeader; + } + + /** + * True if the directory should be shown even if no contacts are found. + */ + public boolean getShowIfEmpty() { + return showIfEmpty; + } + + public boolean getHasHeader() { + return hasHeader; + } + } + + private final Context mContext; + private ArrayList<Partition> mPartitions; + private int mCount = 0; + private boolean mCacheValid = true; + private boolean mNotificationsEnabled = true; + private boolean mNotificationNeeded; + + public CompositeCursorAdapter(Context context) { + this(context, INITIAL_CAPACITY); + } + + public CompositeCursorAdapter(Context context, int initialCapacity) { + mContext = context; + mPartitions = new ArrayList<Partition>(); + } + + public Context getContext() { + return mContext; + } + + /** + * Registers a partition. The cursor for that partition can be set later. + * Partitions should be added in the order they are supposed to appear in the + * list. + */ + public void addPartition(boolean showIfEmpty, boolean hasHeader) { + addPartition(new Partition(showIfEmpty, hasHeader)); + } + + public void addPartition(Partition partition) { + mPartitions.add(partition); + invalidate(); + notifyDataSetChanged(); + } + + public void addPartition(int location, Partition partition) { + mPartitions.add(location, partition); + invalidate(); + notifyDataSetChanged(); + } + + public void removePartition(int partitionIndex) { + Cursor cursor = mPartitions.get(partitionIndex).cursor; + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + mPartitions.remove(partitionIndex); + invalidate(); + notifyDataSetChanged(); + } + + /** + * Removes cursors for all partitions. + */ + // TODO: Is this really what this is supposed to do? Just remove the cursors? Not close them? + // Not remove the partitions themselves? Isn't this leaking? + + public void clearPartitions() { + for (Partition partition : mPartitions) { + partition.cursor = null; + } + invalidate(); + notifyDataSetChanged(); + } + + /** + * Closes all cursors and removes all partitions. + */ + public void close() { + for (Partition partition : mPartitions) { + Cursor cursor = partition.cursor; + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + mPartitions.clear(); + invalidate(); + notifyDataSetChanged(); + } + + public void setHasHeader(int partitionIndex, boolean flag) { + mPartitions.get(partitionIndex).hasHeader = flag; + invalidate(); + } + + public void setShowIfEmpty(int partitionIndex, boolean flag) { + mPartitions.get(partitionIndex).showIfEmpty = flag; + invalidate(); + } + + public Partition getPartition(int partitionIndex) { + return mPartitions.get(partitionIndex); + } + + protected void invalidate() { + mCacheValid = false; + } + + public int getPartitionCount() { + return mPartitions.size(); + } + + protected void ensureCacheValid() { + if (mCacheValid) { + return; + } + + mCount = 0; + for (Partition partition : mPartitions) { + Cursor cursor = partition.cursor; + int count = cursor != null ? cursor.getCount() : 0; + if (partition.hasHeader) { + if (count != 0 || partition.showIfEmpty) { + count++; + } + } + partition.count = count; + mCount += count; + } + + mCacheValid = true; + } + + /** + * Returns true if the specified partition was configured to have a header. + */ + public boolean hasHeader(int partition) { + return mPartitions.get(partition).hasHeader; + } + + /** + * Returns the total number of list items in all partitions. + */ + public int getCount() { + ensureCacheValid(); + return mCount; + } + + /** + * Returns the cursor for the given partition + */ + public Cursor getCursor(int partition) { + return mPartitions.get(partition).cursor; + } + + /** + * Changes the cursor for an individual partition. + */ + public void changeCursor(int partition, Cursor cursor) { + Cursor prevCursor = mPartitions.get(partition).cursor; + if (prevCursor != cursor) { + if (prevCursor != null && !prevCursor.isClosed()) { + prevCursor.close(); + } + mPartitions.get(partition).cursor = cursor; + if (cursor != null) { + mPartitions.get(partition).idColumnIndex = cursor.getColumnIndex("_id"); + } + invalidate(); + notifyDataSetChanged(); + } + } + + /** + * Returns true if the specified partition has no cursor or an empty cursor. + */ + public boolean isPartitionEmpty(int partition) { + Cursor cursor = mPartitions.get(partition).cursor; + return cursor == null || cursor.getCount() == 0; + } + + /** + * Given a list position, returns the index of the corresponding partition. + */ + public int getPartitionForPosition(int position) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + return i; + } + start = end; + } + return -1; + } + + /** + * Given a list position, return the offset of the corresponding item in its + * partition. The header, if any, will have offset -1. + */ + public int getOffsetInPartition(int position) { + ensureCacheValid(); + int start = 0; + for (Partition partition : mPartitions) { + int end = start + partition.count; + if (position >= start && position < end) { + int offset = position - start; + if (partition.hasHeader) { + offset--; + } + return offset; + } + start = end; + } + return -1; + } + + /** + * Returns the first list position for the specified partition. + */ + public int getPositionForPartition(int partition) { + ensureCacheValid(); + int position = 0; + for (int i = 0; i < partition; i++) { + position += mPartitions.get(i).count; + } + return position; + } + + @Override + public int getViewTypeCount() { + return getItemViewTypeCount() + 1; + } + + /** + * Returns the overall number of item view types across all partitions. An + * implementation of this method needs to ensure that the returned count is + * consistent with the values returned by {@link #getItemViewType(int,int)}. + */ + public int getItemViewTypeCount() { + return 1; + } + + /** + * Returns the view type for the list item at the specified position in the + * specified partition. + */ + protected int getItemViewType(int partition, int position) { + return 1; + } + + @Override + public int getItemViewType(int position) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartitions.get(i).hasHeader) { + offset--; + } + if (offset == -1) { + return IGNORE_ITEM_VIEW_TYPE; + } else { + return getItemViewType(i, offset); + } + } + start = end; + } + + throw new ArrayIndexOutOfBoundsException(position); + } + + public View getView(int position, View convertView, ViewGroup parent) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartitions.get(i).hasHeader) { + offset--; + } + View view; + if (offset == -1) { + view = getHeaderView(i, mPartitions.get(i).cursor, convertView, parent); + } else { + if (!mPartitions.get(i).cursor.moveToPosition(offset)) { + throw new IllegalStateException("Couldn't move cursor to position " + + offset); + } + view = getView(i, mPartitions.get(i).cursor, offset, convertView, parent); + } + if (view == null) { + throw new NullPointerException("View should not be null, partition: " + i + + " position: " + offset); + } + return view; + } + start = end; + } + + throw new ArrayIndexOutOfBoundsException(position); + } + + /** + * Returns the header view for the specified partition, creating one if needed. + */ + protected View getHeaderView(int partition, Cursor cursor, View convertView, + ViewGroup parent) { + View view = convertView != null + ? convertView + : newHeaderView(mContext, partition, cursor, parent); + bindHeaderView(view, partition, cursor); + return view; + } + + /** + * Creates the header view for the specified partition. + */ + protected View newHeaderView(Context context, int partition, Cursor cursor, + ViewGroup parent) { + return null; + } + + /** + * Binds the header view for the specified partition. + */ + protected void bindHeaderView(View view, int partition, Cursor cursor) { + } + + /** + * Returns an item view for the specified partition, creating one if needed. + */ + protected View getView(int partition, Cursor cursor, int position, View convertView, + ViewGroup parent) { + View view; + if (convertView != null) { + view = convertView; + } else { + view = newView(mContext, partition, cursor, position, parent); + } + bindView(view, partition, cursor, position); + return view; + } + + /** + * Creates an item view for the specified partition and position. Position + * corresponds directly to the current cursor position. + */ + protected abstract View newView(Context context, int partition, Cursor cursor, int position, + ViewGroup parent); + + /** + * Binds an item view for the specified partition and position. Position + * corresponds directly to the current cursor position. + */ + protected abstract void bindView(View v, int partition, Cursor cursor, int position); + + /** + * Returns a pre-positioned cursor for the specified list position. + */ + public Object getItem(int position) { + ensureCacheValid(); + int start = 0; + for (Partition mPartition : mPartitions) { + int end = start + mPartition.count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartition.hasHeader) { + offset--; + } + if (offset == -1) { + return null; + } + Cursor cursor = mPartition.cursor; + cursor.moveToPosition(offset); + return cursor; + } + start = end; + } + + return null; + } + + /** + * Returns the item ID for the specified list position. + */ + public long getItemId(int position) { + ensureCacheValid(); + int start = 0; + for (Partition mPartition : mPartitions) { + int end = start + mPartition.count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartition.hasHeader) { + offset--; + } + if (offset == -1) { + return 0; + } + if (mPartition.idColumnIndex == -1) { + return 0; + } + + Cursor cursor = mPartition.cursor; + if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) { + return 0; + } + return cursor.getLong(mPartition.idColumnIndex); + } + start = end; + } + + return 0; + } + + /** + * Returns false if any partition has a header. + */ + @Override + public boolean areAllItemsEnabled() { + for (Partition mPartition : mPartitions) { + if (mPartition.hasHeader) { + return false; + } + } + return true; + } + + /** + * Returns true for all items except headers. + */ + @Override + public boolean isEnabled(int position) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartitions.get(i).hasHeader && offset == 0) { + return false; + } else { + return isEnabled(i, offset); + } + } + start = end; + } + + return false; + } + + /** + * Returns true if the item at the specified offset of the specified + * partition is selectable and clickable. + */ + protected boolean isEnabled(int partition, int position) { + return true; + } + + /** + * Enable or disable data change notifications. It may be a good idea to + * disable notifications before making changes to several partitions at once. + */ + public void setNotificationsEnabled(boolean flag) { + mNotificationsEnabled = flag; + if (flag && mNotificationNeeded) { + notifyDataSetChanged(); + } + } + + @Override + public void notifyDataSetChanged() { + if (mNotificationsEnabled) { + mNotificationNeeded = false; + super.notifyDataSetChanged(); + } else { + mNotificationNeeded = true; + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/list/PinnedHeaderListAdapter.java b/src/com/android/launcher3/list/PinnedHeaderListAdapter.java new file mode 100644 index 000000000..cc053e18f --- /dev/null +++ b/src/com/android/launcher3/list/PinnedHeaderListAdapter.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2010 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.list; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +/** + * A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers. + */ +public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter + implements PinnedHeaderListView.PinnedHeaderAdapter { + + public static final int PARTITION_HEADER_TYPE = 0; + + private boolean mPinnedPartitionHeadersEnabled; + private boolean mHeaderVisibility[]; + + public PinnedHeaderListAdapter(Context context) { + super(context); + } + + public PinnedHeaderListAdapter(Context context, int initialCapacity) { + super(context, initialCapacity); + } + + public boolean getPinnedPartitionHeadersEnabled() { + return mPinnedPartitionHeadersEnabled; + } + + public void setPinnedPartitionHeadersEnabled(boolean flag) { + this.mPinnedPartitionHeadersEnabled = flag; + } + + @Override + public int getPinnedHeaderCount() { + if (mPinnedPartitionHeadersEnabled) { + return getPartitionCount(); + } else { + return 0; + } + } + + protected boolean isPinnedPartitionHeaderVisible(int partition) { + return getPinnedPartitionHeadersEnabled() && hasHeader(partition) + && !isPartitionEmpty(partition); + } + + /** + * The default implementation creates the same type of view as a normal + * partition header. + */ + @Override + public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) { + if (hasHeader(partition)) { + View view = null; + if (convertView != null) { + Integer headerType = (Integer)convertView.getTag(); + if (headerType != null && headerType == PARTITION_HEADER_TYPE) { + view = convertView; + } + } + if (view == null) { + view = newHeaderView(getContext(), partition, null, parent); + view.setTag(PARTITION_HEADER_TYPE); + view.setFocusable(false); + view.setEnabled(false); + } + bindHeaderView(view, partition, getCursor(partition)); + view.setLayoutDirection(parent.getLayoutDirection()); + return view; + } else { + return null; + } + } + + @Override + public void configurePinnedHeaders(PinnedHeaderListView listView) { + if (!getPinnedPartitionHeadersEnabled()) { + return; + } + + int size = getPartitionCount(); + boolean unCached = false; + // Cache visibility bits, because we will need them several times later on + if (mHeaderVisibility == null || mHeaderVisibility.length != size) { + mHeaderVisibility = new boolean[size]; + unCached = true; + } + for (int i = 0; i < size; i++) { + boolean visible = isPinnedPartitionHeaderVisible(i); + mHeaderVisibility[i] = visible; + if (!visible) { + listView.setHeaderInvisible(i, true); + } + } + + int headerViewsCount = listView.getHeaderViewsCount(); + + // Starting at the top, find and pin headers for partitions preceding the visible one(s) + int topHeaderHeight = 0; + for (int i = 0; i < size; i++) { + if (mHeaderVisibility[i]) { + int position = listView.getPositionAt(topHeaderHeight) - headerViewsCount; + int partition = getPartitionForPosition(position); + if (i > partition) { + break; + } + + if (!unCached){ + listView.setHeaderPinnedAtTop(i, topHeaderHeight, false); + topHeaderHeight += listView.getPinnedHeaderHeight(i); + } + + } + } + } + + @Override + public int getScrollPositionForHeader(int viewIndex) { + return getPositionForPartition(viewIndex); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/list/PinnedHeaderListView.java b/src/com/android/launcher3/list/PinnedHeaderListView.java new file mode 100644 index 000000000..58e8791ab --- /dev/null +++ b/src/com/android/launcher3/list/PinnedHeaderListView.java @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2010 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.list; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ListAdapter; + +/** + * A ListView that maintains a header pinned at the top of the list. The + * pinned header can be pushed up and dissolved as needed. + */ +public class PinnedHeaderListView extends AutoScrollListView + implements OnScrollListener, OnItemSelectedListener { + + /** + * Adapter interface. The list adapter must implement this interface. + */ + public interface PinnedHeaderAdapter { + + /** + * Returns the overall number of pinned headers, visible or not. + */ + int getPinnedHeaderCount(); + + /** + * Creates or updates the pinned header view. + */ + View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent); + + /** + * Configures the pinned headers to match the visible list items. The + * adapter should call {@link PinnedHeaderListView#setHeaderPinnedAtTop}, + * {@link PinnedHeaderListView#setHeaderPinnedAtBottom}, + * {@link PinnedHeaderListView#setFadingHeader} or + * {@link PinnedHeaderListView#setHeaderInvisible}, for each header that + * needs to change its position or visibility. + */ + void configurePinnedHeaders(PinnedHeaderListView listView); + + /** + * Returns the list position to scroll to if the pinned header is touched. + * Return -1 if the list does not need to be scrolled. + */ + int getScrollPositionForHeader(int viewIndex); + } + + private static final int MAX_ALPHA = 255; + private static final int TOP = 0; + private static final int BOTTOM = 1; + private static final int FADING = 2; + + private static final int DEFAULT_ANIMATION_DURATION = 20; + + private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 100; + + private static final class PinnedHeader { + View view; + boolean visible; + int y; + int height; + int alpha; + int state; + + boolean animating; + boolean targetVisible; + int sourceY; + int targetY; + long targetTime; + } + + private PinnedHeaderAdapter mAdapter; + private int mSize; + private PinnedHeader[] mHeaders; + private RectF mBounds = new RectF(); + private Rect mClipRect = new Rect(); + private OnScrollListener mOnScrollListener; + private OnItemSelectedListener mOnItemSelectedListener; + private int mScrollState; + + private boolean mScrollToSectionOnHeaderTouch = false; + private boolean mHeaderTouched = false; + + private int mAnimationDuration = DEFAULT_ANIMATION_DURATION; + private boolean mAnimating; + private long mAnimationTargetTime; + private int mHeaderPaddingStart; + private int mHeaderWidth; + + public PinnedHeaderListView(Context context) { + this(context, null, android.R.attr.listViewStyle); + } + + public PinnedHeaderListView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.listViewStyle); + } + + public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + super.setOnScrollListener(this); + super.setOnItemSelectedListener(this); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mHeaderPaddingStart = getPaddingStart(); + mHeaderWidth = r - l - mHeaderPaddingStart - getPaddingEnd(); + } + + public void setPinnedHeaderAnimationDuration(int duration) { + mAnimationDuration = duration; + } + + @Override + public void setAdapter(ListAdapter adapter) { + mAdapter = (PinnedHeaderAdapter)adapter; + super.setAdapter(adapter); + } + + @Override + public void setOnScrollListener(OnScrollListener onScrollListener) { + mOnScrollListener = onScrollListener; + super.setOnScrollListener(this); + } + + @Override + public void setOnItemSelectedListener(OnItemSelectedListener listener) { + mOnItemSelectedListener = listener; + super.setOnItemSelectedListener(this); + } + + public void setScrollToSectionOnHeaderTouch(boolean value) { + mScrollToSectionOnHeaderTouch = value; + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + if (mAdapter != null) { + int count = mAdapter.getPinnedHeaderCount(); + if (count != mSize) { + mSize = count; + if (mHeaders == null) { + mHeaders = new PinnedHeader[mSize]; + } else if (mHeaders.length < mSize) { + PinnedHeader[] headers = mHeaders; + mHeaders = new PinnedHeader[mSize]; + System.arraycopy(headers, 0, mHeaders, 0, headers.length); + } + } + + for (int i = 0; i < mSize; i++) { + if (mHeaders[i] == null) { + mHeaders[i] = new PinnedHeader(); + } + mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this); + } + + mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration; + mAdapter.configurePinnedHeaders(this); + invalidateIfAnimating(); + + } + if (mOnScrollListener != null) { + mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount); + } + } + + @Override + protected float getTopFadingEdgeStrength() { + // Disable vertical fading at the top when the pinned header is present + return mSize > 0 ? 0 : super.getTopFadingEdgeStrength(); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mScrollState = scrollState; + if (mOnScrollListener != null) { + mOnScrollListener.onScrollStateChanged(this, scrollState); + } + } + + /** + * Ensures that the selected item is positioned below the top-pinned headers + * and above the bottom-pinned ones. + */ + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + int height = getHeight(); + + int windowTop = 0; + int windowBottom = height; + + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + if (header.state == TOP) { + windowTop = header.y + header.height; + } else if (header.state == BOTTOM) { + windowBottom = header.y; + break; + } + } + } + + View selectedView = getSelectedView(); + if (selectedView != null) { + if (selectedView.getTop() < windowTop) { + setSelectionFromTop(position, windowTop); + } else if (selectedView.getBottom() > windowBottom) { + setSelectionFromTop(position, windowBottom - selectedView.getHeight()); + } + } + + if (mOnItemSelectedListener != null) { + mOnItemSelectedListener.onItemSelected(parent, view, position, id); + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + if (mOnItemSelectedListener != null) { + mOnItemSelectedListener.onNothingSelected(parent); + } + } + + public int getPinnedHeaderHeight(int viewIndex) { + ensurePinnedHeaderLayout(viewIndex); + return mHeaders[viewIndex].view.getHeight(); + } + + /** + * Set header to be pinned at the top. + * + * @param viewIndex index of the header view + * @param y is position of the header in pixels. + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) { + ensurePinnedHeaderLayout(viewIndex); + PinnedHeader header = mHeaders[viewIndex]; + header.visible = true; + header.y = y; + header.state = TOP; + + // TODO perhaps we should animate at the top as well + header.animating = false; + } + + /** + * Set header to be pinned at the bottom. + * + * @param viewIndex index of the header view + * @param y is position of the header in pixels. + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) { + ensurePinnedHeaderLayout(viewIndex); + PinnedHeader header = mHeaders[viewIndex]; + header.state = BOTTOM; + if (header.animating) { + header.targetTime = mAnimationTargetTime; + header.sourceY = header.y; + header.targetY = y; + } else if (animate && (header.y != y || !header.visible)) { + if (header.visible) { + header.sourceY = header.y; + } else { + header.visible = true; + header.sourceY = y + header.height; + } + header.animating = true; + header.targetVisible = true; + header.targetTime = mAnimationTargetTime; + header.targetY = y; + } else { + header.visible = true; + header.y = y; + } + } + + /** + * Set header to be pinned at the top of the first visible item. + * + * @param viewIndex index of the header view + * @param position is position of the header in pixels. + */ + public void setFadingHeader(int viewIndex, int position, boolean fade) { + ensurePinnedHeaderLayout(viewIndex); + + View child = getChildAt(position - getFirstVisiblePosition()); + if (child == null) return; + + PinnedHeader header = mHeaders[viewIndex]; + header.visible = true; + header.state = FADING; + header.alpha = MAX_ALPHA; + header.animating = false; + + int top = getTotalTopPinnedHeaderHeight(); + header.y = top; + if (fade) { + int bottom = child.getBottom() - top; + int headerHeight = header.height; + if (bottom < headerHeight) { + int portion = bottom - headerHeight; + header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight; + header.y = top + portion; + } + } + } + + /** + * Makes header invisible. + * + * @param viewIndex index of the header view + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderInvisible(int viewIndex, boolean animate) { + PinnedHeader header = mHeaders[viewIndex]; + if (header.visible && (animate || header.animating) && header.state == BOTTOM) { + header.sourceY = header.y; + if (!header.animating) { + header.visible = true; + header.targetY = getBottom() + header.height; + } + header.animating = true; + header.targetTime = mAnimationTargetTime; + header.targetVisible = false; + } else { + header.visible = false; + } + } + + private void ensurePinnedHeaderLayout(int viewIndex) { + View view = mHeaders[viewIndex].view; + if (view.isLayoutRequested()) { + int widthSpec = View.MeasureSpec.makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY); + int heightSpec; + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + if (layoutParams != null && layoutParams.height > 0) { + heightSpec = View.MeasureSpec + .makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY); + } else { + heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + } + view.measure(widthSpec, heightSpec); + int height = view.getMeasuredHeight(); + mHeaders[viewIndex].height = height; + view.layout(0, 0, mHeaderWidth, height); + } + } + + /** + * Returns the sum of heights of headers pinned to the top. + */ + public int getTotalTopPinnedHeaderHeight() { + for (int i = mSize; --i >= 0;) { + PinnedHeader header = mHeaders[i]; + if (header.visible && header.state == TOP) { + return header.y + header.height; + } + } + return 0; + } + + /** + * Returns the list item position at the specified y coordinate. + */ + public int getPositionAt(int y) { + do { + int position = pointToPosition(getPaddingLeft() + 1, y); + if (position != -1) { + return position; + } + // If position == -1, we must have hit a separator. Let's examine + // a nearby pixel + y--; + } while (y > 0); + return 0; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + mHeaderTouched = false; + if (super.onInterceptTouchEvent(ev)) { + return true; + } + + if (mScrollState == SCROLL_STATE_IDLE) { + final int y = (int)ev.getY(); + final int x = (int)ev.getX(); + for (int i = mSize; --i >= 0;) { + PinnedHeader header = mHeaders[i]; + // For RTL layouts, this also takes into account that the scrollbar is on the left + // side. + final int padding = getPaddingLeft(); + if (header.visible && header.y <= y && header.y + header.height > y && + x >= padding && padding + mHeaderWidth >= x) { + mHeaderTouched = true; + if (mScrollToSectionOnHeaderTouch && + ev.getAction() == MotionEvent.ACTION_DOWN) { + return smoothScrollToPartition(i); + } else { + return true; + } + } + } + } + + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mHeaderTouched) { + if (ev.getAction() == MotionEvent.ACTION_UP) { + mHeaderTouched = false; + } + return true; + } + return super.onTouchEvent(ev); + }; + + private boolean smoothScrollToPartition(int partition) { + final int position = mAdapter.getScrollPositionForHeader(partition); + if (position == -1) { + return false; + } + + int offset = 0; + for (int i = 0; i < partition; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + offset += header.height; + } + } + smoothScrollToPositionFromTop(position + getHeaderViewsCount(), offset, + DEFAULT_SMOOTH_SCROLL_DURATION); + return true; + } + + private void invalidateIfAnimating() { + mAnimating = false; + for (int i = 0; i < mSize; i++) { + if (mHeaders[i].animating) { + mAnimating = true; + invalidate(); + return; + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + long currentTime = mAnimating ? System.currentTimeMillis() : 0; + + int top = 0; + int bottom = getBottom(); + boolean hasVisibleHeaders = false; + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + hasVisibleHeaders = true; + if (header.state == BOTTOM && header.y < bottom) { + bottom = header.y; + } else if (header.state == TOP || header.state == FADING) { + int newTop = header.y + header.height; + if (newTop > top) { + top = newTop; + } + } + } + } + + if (hasVisibleHeaders) { + canvas.save(); + mClipRect.set(0, top, getWidth(), bottom); + canvas.clipRect(mClipRect); + } + + super.dispatchDraw(canvas); + + if (hasVisibleHeaders) { + canvas.restore(); + + // First draw top headers, then the bottom ones to handle the Z axis correctly + for (int i = mSize; --i >= 0;) { + PinnedHeader header = mHeaders[i]; + if (header.visible && (header.state == TOP || header.state == FADING)) { + drawHeader(canvas, header, currentTime); + } + } + + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible && header.state == BOTTOM) { + drawHeader(canvas, header, currentTime); + } + } + } + + invalidateIfAnimating(); + } + + private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) { + if (header.animating) { + int timeLeft = (int)(header.targetTime - currentTime); + if (timeLeft <= 0) { + header.y = header.targetY; + header.visible = header.targetVisible; + header.animating = false; + } else { + header.y = header.targetY + (header.sourceY - header.targetY) * timeLeft + / mAnimationDuration; + } + } + if (header.visible) { + View view = header.view; + int saveCount = canvas.save(); + canvas.translate(isLayoutRtl() ? + getWidth() - mHeaderPaddingStart - mHeaderWidth : mHeaderPaddingStart, + header.y); + if (header.state == FADING) { + mBounds.set(0, 0, mHeaderWidth, view.getHeight()); + canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG); + } + view.draw(canvas); + canvas.restoreToCount(saveCount); + } + } + + /** + * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. + */ + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java b/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java new file mode 100644 index 000000000..1ccd2daa0 --- /dev/null +++ b/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java @@ -0,0 +1,434 @@ +package com.android.launcher3.list; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.OverviewSettingsPanel; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.settings.SettingsProvider; + +public class SettingsPinnedHeaderAdapter extends PinnedHeaderListAdapter { + public static final String ACTION_SEARCH_BAR_VISIBILITY_CHANGED = + "cyanogenmod.intent.action.SEARCH_BAR_VISIBILITY_CHANGED"; + + private Launcher mLauncher; + private Context mContext; + + class SettingsPosition { + int partition = 0; + int position = 0; + + SettingsPosition (int partition, int position) { + this.partition = partition; + this.position = position; + } + } + + public SettingsPinnedHeaderAdapter(Context context) { + super(context); + mLauncher = (Launcher) context; + mContext = context; + } + + private String[] mHeaders; + public int mPinnedHeaderCount; + + public void setHeaders(String[] headers) { + this.mHeaders = headers; + } + + @Override + protected View newHeaderView(Context context, int partition, Cursor cursor, + ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(context); + return inflater.inflate(R.layout.settings_pane_list_header, null); + } + + @Override + protected void bindHeaderView(View view, int partition, Cursor cursor) { + TextView textView = (TextView) view.findViewById(R.id.item_name); + textView.setText(mHeaders[partition]); + } + + @Override + protected View newView(Context context, int partition, Cursor cursor, int position, + ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(context); + return inflater.inflate(R.layout.settings_pane_list_item, null); + } + + @Override + protected void bindView(View v, int partition, Cursor cursor, int position) { + TextView nameView = (TextView)v.findViewById(R.id.item_name); + TextView stateView = (TextView)v.findViewById(R.id.item_state); + Switch settingSwitch = (Switch)v.findViewById(R.id.setting_switch); + settingSwitch.setClickable(false); + + // RTL + Configuration config = mLauncher.getResources().getConfiguration(); + if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + nameView.setGravity(Gravity.RIGHT); + } + + String title = cursor.getString(1); + nameView.setText(title); + + v.setTag(new SettingsPosition(partition, position)); + + Resources res = mLauncher.getResources(); + + boolean current; + String state; + + switch (partition) { + case OverviewSettingsPanel.HOME_SETTINGS_POSITION: + switch (position) { + case 0: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SEARCH, + R.bool.preferences_interface_homescreen_search_default); + setSettingSwitch(stateView, settingSwitch, current); + break; + case 1: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + // Reversed logic here. Boolean is hideLabels, where setting is show labels + setSettingSwitch(stateView, settingSwitch, !current); + break; + case 2: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL, + R.bool.preferences_interface_homescreen_scrolling_wallpaper_scroll_default); + setSettingSwitch(stateView, settingSwitch, current); + break; + case 3: + updateDynamicGridSizeSettingsItem(stateView, settingSwitch); + break; + case 4: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_ALLOW_ROTATION, + R.bool.preferences_interface_allow_rotation); + setSettingSwitch(stateView, settingSwitch, current); + break; + case 5: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_REMOTE_FOLDER, + R.bool.preferences_interface_homescreen_remote_folder_default); + setSettingSwitch(stateView, settingSwitch, current); + break; + default: + hideStates(stateView, settingSwitch); + } + break; + case OverviewSettingsPanel.DRAWER_SETTINGS_POSITION: + switch (position) { + case 0: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, + R.bool.preferences_interface_drawer_hide_icon_labels_default); + // Reversed logic here. Boolean is hideLabels, where setting is show labels + setSettingSwitch(stateView, settingSwitch, !current); + break; + case 1: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_DRAWER_STYLE_USE_COMPACT, + R.bool.preferences_interface_drawer_compact_default); + state = current ? res.getString(R.string.app_drawer_style_compact) + : res.getString(R.string.app_drawer_style_sections); + setStateText(stateView, settingSwitch, state); + break; + case 2: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_DRAWER_DARK, + R.bool.preferences_interface_drawer_dark_default); + state = current ? res.getString(R.string.app_drawer_color_dark) + : res.getString(R.string.app_drawer_color_light); + setStateText(stateView, settingSwitch, state); + break; + case 3: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_USE_SCROLLER, + R.bool.preferences_interface_use_scroller_default); + setSettingSwitch(stateView, settingSwitch, current); + break; + case 4: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_USE_HORIZONTAL_SCRUBBER, + R.bool.preferences_interface_use_horizontal_scrubber_default); + state = current ? res.getString(R.string.fast_scroller_type_horizontal) + : res.getString(R.string.fast_scroller_type_vertical); + setStateText(stateView, settingSwitch, state); + break; + case 5: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_DRAWER_SEARCH, + R.bool.preferences_interface_drawer_search_default); + setSettingSwitch(stateView, settingSwitch, current); + break; + case 6: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_DRAWER_REMOTE_APPS, + R.bool.preferences_interface_drawer_remote_apps_default); + setSettingSwitch(stateView, settingSwitch, current); + break; + default: + hideStates(stateView, settingSwitch); + } + break; + case OverviewSettingsPanel.APP_SETTINGS_POSITION: + switch (position) { + case 0: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_GENERAL_ICONS_LARGE, + R.bool.preferences_interface_general_icons_large_default); + setSettingSwitch(stateView, settingSwitch, current); + break; + default: + hideStates(stateView, settingSwitch); + } + } + + v.setOnClickListener(mSettingsItemListener); + } + + @Override + public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + View view = inflater.inflate(R.layout.settings_pane_list_header, parent, false); + view.setFocusable(false); + view.setEnabled(false); + bindHeaderView(view, viewIndex, null); + return view; + } + + @Override + public int getPinnedHeaderCount() { + return mPinnedHeaderCount; + } + + public void updateDynamicGridSizeSettingsItem(TextView stateView, Switch settingSwitch) { + InvariantDeviceProfile.GridSize gridSize = InvariantDeviceProfile.GridSize.getModeForValue( + SettingsProvider.getIntCustomDefault(mLauncher, + SettingsProvider.SETTINGS_UI_DYNAMIC_GRID_SIZE, 0)); + String state = ""; + + switch (gridSize) { + case Comfortable: + state = mLauncher.getResources().getString(R.string.grid_size_comfortable); + break; + case Cozy: + state = mLauncher.getResources().getString(R.string.grid_size_cozy); + break; + case Condensed: + state = mLauncher.getResources().getString(R.string.grid_size_condensed); + break; + case Custom: + int rows = SettingsProvider.getIntCustomDefault(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_ROWS, 0); + int columns = SettingsProvider.getIntCustomDefault(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_COLUMNS, 0); + state = rows + " " + "\u00d7" + " " + columns; + break; + } + setStateText(stateView, settingSwitch, state); + } + + OnClickListener mSettingsItemListener = new OnClickListener() { + + @Override + public void onClick(View v) { + int partition = ((SettingsPosition) v.getTag()).partition; + int position = ((SettingsPosition) v.getTag()).position; + + switch (partition) { + case OverviewSettingsPanel.HOME_SETTINGS_POSITION: + switch (position) { + case 0: + updateSearchBarVisibility(v); + break; + case 1: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default, + true); + mLauncher.reloadLauncher(false, false); + break; + case 2: + onSettingsBooleanChanged(v, + SettingsProvider + .SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL, + R.bool.preferences_interface_homescreen_scrolling_wallpaper_scroll_default, + false); + mLauncher.reloadLauncher(false, false); + break; + case 3: + mLauncher.onClickDynamicGridSizeButton(); + break; + case 4: + String key = SettingsProvider.SETTINGS_UI_ALLOW_ROTATION; + boolean newValue = onSettingsBooleanChanged(v, key, + R.bool.preferences_interface_allow_rotation, false); + + Bundle extras = new Bundle(); + extras.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, newValue); + + // Required for system to pickup rotation change. + mContext.getContentResolver().call( + LauncherSettings.Settings.CONTENT_URI, + LauncherSettings.Settings.METHOD_SET_BOOLEAN, + key, extras); + + break; + case 5: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_HOMESCREEN_REMOTE_FOLDER, + R.bool.preferences_interface_homescreen_remote_folder_default, + false); + mLauncher.getRemoteFolderManager().onSettingChanged(); + break; + } + break; + case OverviewSettingsPanel.DRAWER_SETTINGS_POSITION: + switch (position) { + case 0: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, + R.bool.preferences_interface_drawer_hide_icon_labels_default, + true); + mLauncher.reloadAppDrawer(); + break; + case 1: + onTextSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_DRAWER_STYLE_USE_COMPACT, + R.bool.preferences_interface_drawer_compact_default, + R.string.app_drawer_style_compact, + R.string.app_drawer_style_sections); + mLauncher.reloadAppDrawer(); + break; + case 2: + onTextSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_DRAWER_DARK, + R.bool.preferences_interface_drawer_dark_default, + R.string.app_drawer_color_dark, + R.string.app_drawer_color_light); + mLauncher.reloadAppDrawer(); + break; + case 3: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_USE_SCROLLER, + R.bool.preferences_interface_use_scroller_default, false); + mLauncher.reloadAppDrawer(); + mLauncher.reloadWidgetView(); + break; + case 4: + onTextSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_USE_HORIZONTAL_SCRUBBER, + R.bool.preferences_interface_use_horizontal_scrubber_default, + R.string.fast_scroller_type_horizontal, + R.string.fast_scroller_type_vertical); + mLauncher.reloadAppDrawer(); + mLauncher.reloadWidgetView(); + break; + case 5: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_DRAWER_SEARCH, + R.bool.preferences_interface_drawer_search_default, false); + mLauncher.reloadAppDrawer(); + break; + case 6: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_DRAWER_REMOTE_APPS, + R.bool.preferences_interface_drawer_remote_apps_default, false); + mLauncher.getRemoteFolderManager().onSettingChanged(); + break; + } + break; + case OverviewSettingsPanel.APP_SETTINGS_POSITION: + switch (position) { + case 0: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_GENERAL_ICONS_LARGE, + R.bool.preferences_interface_general_icons_large_default, false); + mLauncher.reloadLauncher(true, true); + break; + case 1: + Intent intent = new Intent(); + intent.setClassName(OverviewSettingsPanel.ANDROID_SETTINGS, + OverviewSettingsPanel.ANDROID_PROTECTED_APPS); + mLauncher.startActivity(intent); + break; + } + } + + View defaultHome = mLauncher.findViewById(R.id.default_home_screen_panel); + defaultHome.setVisibility(getCursor(0).getCount() > 1 ? View.VISIBLE : View.GONE); + } + }; + + private void updateSearchBarVisibility(View v) { + boolean isSearchEnabled = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SEARCH, + R.bool.preferences_interface_homescreen_search_default); + + if (!isSearchEnabled) { + if (!Utilities.searchActivityExists(mContext)) { + Toast.makeText(mContext, mContext.getString(R.string.search_activity_not_found), + Toast.LENGTH_SHORT).show(); + return; + } + } + + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SEARCH, + R.bool.preferences_interface_homescreen_search_default, false); + + Intent intent = new Intent(ACTION_SEARCH_BAR_VISIBILITY_CHANGED); + mContext.sendBroadcast(intent); + } + + private boolean onSettingsBooleanChanged(View v, String key, int res, boolean invert) { + boolean newValue = SettingsProvider.changeBoolean(mContext, key, res); + ((Switch)v.findViewById(R.id.setting_switch)).setChecked(invert != newValue); + return newValue; + } + + private void onTextSettingsBooleanChanged(View v, String key, int defRes, + int trueRes, int falseRes) { + boolean newValue = SettingsProvider.changeBoolean(mContext, key, defRes); + int state = newValue ? trueRes : falseRes; + ((TextView) v.findViewById(R.id.item_state)).setText(state); + } + + private void setStateText(TextView stateView, Switch settingSwitch, String state) { + stateView.setText(state); + stateView.setVisibility(View.VISIBLE); + settingSwitch.setVisibility(View.INVISIBLE); + } + + private void setSettingSwitch(TextView stateView, Switch settingSwitch, boolean isChecked) { + settingSwitch.setChecked(isChecked); + settingSwitch.setVisibility(View.VISIBLE); + stateView.setVisibility(View.INVISIBLE); + } + + private void hideStates(TextView stateView, Switch settingSwitch) { + settingSwitch.setVisibility(View.INVISIBLE); + stateView.setVisibility(View.INVISIBLE); + } +} diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java index eef4f9173..d0fdad32a 100644 --- a/src/com/android/launcher3/model/WidgetsModel.java +++ b/src/com/android/launcher3/model/WidgetsModel.java @@ -7,15 +7,18 @@ import android.content.pm.ResolveInfo; import android.util.Log; import com.android.launcher3.AppFilter; +import com.android.launcher3.AppInfo; import com.android.launcher3.IconCache; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.ItemInfo; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.ProtectedComponentsHelper; import com.android.launcher3.Utilities; import com.android.launcher3.compat.AlphabeticIndexCompat; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.UserHandleCompat; +import cyanogenmod.providers.CMSettings; import java.util.ArrayList; import java.util.Collections; @@ -41,6 +44,7 @@ public class WidgetsModel { private ArrayList<Object> mRawList; + private Context mContext; private final AppWidgetManagerCompat mAppWidgetMgr; private final WidgetsAndShortcutNameComparator mWidgetAndShortcutNameComparator; private final Comparator<ItemInfo> mAppNameComparator; @@ -49,6 +53,7 @@ public class WidgetsModel { private AlphabeticIndexCompat mIndexer; public WidgetsModel(Context context, IconCache iconCache, AppFilter appFilter) { + mContext = context; mAppWidgetMgr = AppWidgetManagerCompat.getInstance(context); mWidgetAndShortcutNameComparator = new WidgetsAndShortcutNameComparator(context); mAppNameComparator = (new AppNameComparator(context)).getAppInfoComparator(); @@ -110,6 +115,7 @@ public class WidgetsModel { mWidgetAndShortcutNameComparator.reset(); InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile(); + ProtectedComponentsHelper.updateProtectedComponentsLists(mContext); // add and update. for (Object o: rawWidgetsShortcuts) { @@ -158,11 +164,18 @@ public class WidgetsModel { PackageItemInfo pInfo = tmpPackageItemInfos.get(packageName); ArrayList<Object> widgetsShortcutsList = mWidgetsList.get(pInfo); if (widgetsShortcutsList != null) { + if (pInfo != null && ProtectedComponentsHelper.isProtectedPackage(pInfo.flags, + packageName)) { + continue; + } widgetsShortcutsList.add(o); } else { + pInfo = new PackageItemInfo(packageName); + if (ProtectedComponentsHelper.isProtectedPackage(pInfo.flags, packageName)) { + continue; + } widgetsShortcutsList = new ArrayList<>(); widgetsShortcutsList.add(o); - pInfo = new PackageItemInfo(packageName); mIconCache.getTitleAndIconForApp(packageName, userHandle, true /* userLowResIcon */, pInfo); pInfo.titleSectionName = mIndexer.computeSectionName(pInfo.title); diff --git a/src/com/android/launcher3/settings/SettingsProvider.java b/src/com/android/launcher3/settings/SettingsProvider.java new file mode 100644 index 000000000..e809a8a0d --- /dev/null +++ b/src/com/android/launcher3/settings/SettingsProvider.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.settings; + +import android.content.Context; +import android.content.SharedPreferences; + +public final class SettingsProvider { + public static final String SETTINGS_KEY = "trebuchet_preferences"; + + public static final String SETTINGS_UI_HOMESCREEN_DEFAULT_SCREEN_ID = "ui_homescreen_default_screen_id"; + public static final String SETTINGS_UI_HOMESCREEN_SEARCH = "ui_homescreen_search"; + public static final String SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS = "ui_homescreen_general_hide_icon_labels"; + public static final String SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL = "ui_homescreen_scrolling_wallpaper_scroll"; + public static final String SETTINGS_UI_HOMESCREEN_REMOTE_FOLDER = "ui_homescreen_remote_folder"; + public static final String SETTINGS_UI_DRAWER_REMOTE_APPS = "ui_drawer_remote_apps"; + public static final String SETTINGS_UI_DYNAMIC_GRID_SIZE = "ui_dynamic_grid_size"; + public static final String SETTINGS_UI_HOMESCREEN_ROWS = "ui_homescreen_rows"; + public static final String SETTINGS_UI_HOMESCREEN_COLUMNS = "ui_homescreen_columns"; + public static final String SETTINGS_UI_DRAWER_HIDE_ICON_LABELS = "ui_drawer_hide_icon_labels"; + public static final String SETTINGS_UI_DRAWER_STYLE_USE_COMPACT = "ui_drawer_style_compact"; + public static final String SETTINGS_UI_DRAWER_DARK = "ui_drawer_dark"; + public static final String SETTINGS_UI_USE_SCROLLER = "ui_scroller"; + public static final String SETTINGS_UI_USE_HORIZONTAL_SCRUBBER = "ui_horizontal_scrubber"; + public static final String SETTINGS_UI_DRAWER_SEARCH = "ui_drawer_search"; + public static final String SETTINGS_UI_GENERAL_ICONS_LARGE = "ui_general_icons_large"; + public static final String SETTINGS_UI_ALLOW_ROTATION = "ui_allow_rotation"; + + public static SharedPreferences get(Context context) { + return context.getSharedPreferences(SETTINGS_KEY, Context.MODE_PRIVATE); + } + + public static int getIntCustomDefault(Context context, String key, int def) { + return get(context).getInt(key, def); + } + + public static int getInt(Context context, String key, int resource) { + return getIntCustomDefault(context, key, context.getResources().getInteger(resource)); + } + + public static long getLongCustomDefault(Context context, String key, long def) { + return get(context).getLong(key, def); + } + + public static long getLong(Context context, String key, int resource) { + return getLongCustomDefault(context, key, context.getResources().getInteger(resource)); + } + + public static boolean getBooleanCustomDefault(Context context, String key, boolean def) { + return get(context).getBoolean(key, def); + } + + public static boolean getBoolean(Context context, String key, int resource) { + return getBooleanCustomDefault(context, key, context.getResources().getBoolean(resource)); + } + + public static String getStringCustomDefault(Context context, String key, String def) { + return get(context).getString(key, def); + } + + public static String getString(Context context, String key, int resource) { + return getStringCustomDefault(context, key, context.getResources().getString(resource)); + } + + public static void putString(Context context, String key, String value) { + get(context).edit().putString(key, value).commit(); + } + + public static void putInt(Context context, String key, int value) { + get(context).edit().putInt(key, value).commit(); + } + + public static boolean changeBoolean(Context context, String key, int defaultRes) { + boolean def = context.getResources().getBoolean(defaultRes); + boolean val = !SettingsProvider.getBooleanCustomDefault(context, key, def); + putBoolean(context, key, val); + return val; + } + + public static void putBoolean(Context context, String key, int res) { + boolean val = context.getResources().getBoolean(res); + putBoolean(context, key, val); + } + + public static void putBoolean(Context context, String key, boolean value) { + get(context).edit().putBoolean(key, value).commit(); + } +} diff --git a/src/com/android/launcher3/stats/LauncherStats.java b/src/com/android/launcher3/stats/LauncherStats.java new file mode 100644 index 000000000..5e8cb83d5 --- /dev/null +++ b/src/com/android/launcher3/stats/LauncherStats.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2015. The CyanogenMod 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.stats; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; +import com.android.launcher3.stats.internal.db.DatabaseHelper; +import com.android.launcher3.stats.internal.model.TrackingEvent; + +/** + * <pre> + * Utility class made specifically for Launcher related events + * </pre> + */ +public class LauncherStats { + + // Constants + private static final String TAG = LauncherStats.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final int MSG_STORE_EVENT = 1000; + public static final String SETTINGS_PACKAGE_NAME = "com.android.settings"; + public static final String ORIGIN_HOMESCREEN = "homescreen"; + public static final String ORIGIN_APPDRAWER = "appdrawer"; + public static final String ORIGIN_TREB_LONGPRESS = "trebuchet_longpress"; + public static final String ORIGIN_CHOOSER = "theme_chooser"; + public static final String ORIGIN_SETTINGS = "settings"; + public static final String ORIGIN_DRAG_DROP = "drag_drop"; + public static final String ORIGIN_FOLDER = "folder"; + + private static void log(String msg) throws IllegalArgumentException { + if (TextUtils.isEmpty(msg)) { + throw new IllegalArgumentException("'msg' cannot be null or empty!"); + } + if (DEBUG) { + Log.d(TAG, msg); + } + } + + private static void loge(String msg) throws IllegalArgumentException { + if (TextUtils.isEmpty(msg)) { + throw new IllegalArgumentException("'msg' cannot be null or empty!"); + } + Log.e(TAG, msg); + } + + /** + * <pre> + * This is a thread responsible for writing events to a database + * </pre> + * + * @see {@link HandlerThread} + */ + private static class WriteHandlerThread extends HandlerThread { + public WriteHandlerThread() { + super(WriteHandlerThread.class.getSimpleName()); + } + } + + /** + * <pre> + * Handler for issuing db writes + * </pre> + * + * @see {@link Handler} + */ + private static class WriteHandler extends Handler { + + public WriteHandler() { + super(sHandlerThread.getLooper()); + } + + @Override + public void handleMessage(Message msg) { + log("Handling message: " + msg.what); + switch (msg.what) { + case MSG_STORE_EVENT: + handleStoreEvent((TrackingEvent) msg.obj); + break; + default: + super.handleMessage(msg); + } + } + } + + // Instance + private static LauncherStats sInstance = null; + + // Members + private static WriteHandlerThread sHandlerThread; + private static WriteHandler sWriteHandler; + private static DatabaseHelper sDatabaseHelper; + + /** + * Send a message to the handler to store event data + * + * @param trackingEvent {@link TrackingEvent} + */ + protected void sendStoreEventMessage(TrackingEvent trackingEvent) { + log("Sending tracking event to handler: " + trackingEvent); + Message msg = new Message(); + msg.what = MSG_STORE_EVENT; + msg.obj = trackingEvent; + sWriteHandler.sendMessage(msg); + } + + /** + * Handle the storing work + * + * @param trackingEvent {@link TrackingEvent} + */ + private static void handleStoreEvent(TrackingEvent trackingEvent) { + log("Handling store event: " + trackingEvent); + if (trackingEvent != null) { + sDatabaseHelper.writeEvent(trackingEvent); + } else { + loge("Tracking event was null!"); + } + } + + /** + * Used only for overlay extensions + */ + protected LauncherStats() { } + + /** + * Constructor + * + * @param context {@link Context} not null! + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + private LauncherStats(Context context) throws IllegalArgumentException { + if (context == null) { + throw new IllegalArgumentException("'context' cannot be null!"); + } + sDatabaseHelper = new DatabaseHelper(context); + sHandlerThread = new WriteHandlerThread(); + sHandlerThread.start(); + sWriteHandler = new WriteHandler(); + } + + /** + * Gets a singleton instance of the stats utility + * + * @param context {@link Context} not null! + * @return {@link LauncherStats} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static LauncherStats getInstance(Context context) + throws IllegalArgumentException { + if (sInstance == null) { + sInstance = new LauncherStats(context); + } + return sInstance; + } + + /** + * Interface for posting a new widget add event + * + * @param pkg {@link String} package name of widget + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public void sendWidgetAddEvent(String pkg) throws IllegalArgumentException { + if (TextUtils.isEmpty(pkg)) { + throw new IllegalArgumentException("'pkg' cannot be null!"); + } + TrackingEvent trackingEvent = new TrackingEvent(TrackingEvent.Category.WIDGET_ADD); + trackingEvent.setMetaData(TrackingEvent.KEY_PACKAGE, pkg); + sendStoreEventMessage(trackingEvent); + } + + /** + * Interface for posting a new widget removal event + * + * @param pkg {@link String} package name of widget + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public void sendWidgetRemoveEvent(String pkg) throws IllegalArgumentException { + if (TextUtils.isEmpty(pkg)) { + throw new IllegalArgumentException("'pkg' cannot be null!"); + } + TrackingEvent trackingEvent = new TrackingEvent(TrackingEvent.Category.WIDGET_REMOVE); + trackingEvent.setMetaData(TrackingEvent.KEY_PACKAGE, pkg); + sendStoreEventMessage(trackingEvent); + } + + /** + * Interface for posting an app launch event + * + * @param origin {@link String} origin of application launch + * @param pkg {@link String} package of app launched + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public void sendAppLaunchEvent(String origin, String pkg) throws IllegalArgumentException { + if (TextUtils.isEmpty(origin)) { + throw new IllegalArgumentException("'origin' cannot be null!"); + } + if (TextUtils.isEmpty(pkg)) { + throw new IllegalArgumentException("'pkg' cannot be null!"); + } + TrackingEvent trackingEvent = new TrackingEvent(TrackingEvent.Category.APP_LAUNCH); + trackingEvent.setMetaData(TrackingEvent.KEY_ORIGIN, origin); + trackingEvent.setMetaData(TrackingEvent.KEY_PACKAGE, pkg); + sendStoreEventMessage(trackingEvent); + } + + /** + * Interface for sending a "settings opened" event + * + * @param origin {@link String} origin of the event + */ + public void sendSettingsOpenedEvent(String origin) { + TrackingEvent trackingEvent = new TrackingEvent(TrackingEvent.Category.SETTINGS_OPEN); + trackingEvent.setMetaData(TrackingEvent.KEY_ORIGIN, origin); + sendStoreEventMessage(trackingEvent); + } + + /** + * Interface for sending a "wallpaper changed" event + * + * @param origin {@link String} origin of the event + */ + public void sendWallpaperChangedEvent(String origin) { + TrackingEvent trackingEvent = new TrackingEvent(TrackingEvent.Category.WALLPAPER_CHANGE); + trackingEvent.setMetaData(TrackingEvent.KEY_ORIGIN, origin); + sendStoreEventMessage(trackingEvent); + } + +} diff --git a/src/com/android/launcher3/stats/external/StatsUtil.java b/src/com/android/launcher3/stats/external/StatsUtil.java new file mode 100644 index 000000000..697df542c --- /dev/null +++ b/src/com/android/launcher3/stats/external/StatsUtil.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2015. The CyanogenMod 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.stats.external; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.util.Log; +import com.android.launcher3.stats.util.Logger; + +/** + * StatsUtil + * <pre> + * Utility for interfacing with CyanogenStats + * </pre> + */ +public class StatsUtil { + + // Tag and logging + private static final String TAG = StatsUtil.class.getSimpleName(); + + // Constants + private static final String KEY_TRACKING_ID = "tracking_id"; + private static final String ANALYTIC_INTENT = "com.cyngn.stats.action.SEND_ANALYTIC_EVENT"; + private static final String STATS_PACKAGE = "com.cyngn.stats"; + + /** + * Checks if stats collection is enabled + * + * @param context {@link android.content.Context} + * @return {@link java.lang.Boolean} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static boolean isStatsCollectionEnabled(Context context) + throws IllegalArgumentException { + return isStatsPackageInstalledAndSystemApp(context); + } + + /** + * Checks if the stats package is installed + * + * @param context {@link android.content.Context} + * @return {@link Boolean {@link Boolean {@link Boolean {@link Boolean}}}} + */ + private static boolean isStatsPackageInstalledAndSystemApp(Context context) + throws IllegalArgumentException { + if (context == null) { + throw new IllegalArgumentException("'context' cannot be null!"); + } + try { + PackageInfo pi = context.getPackageManager().getPackageInfo(STATS_PACKAGE, 0); + boolean isSystemApp = (pi.applicationInfo.flags & + (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0; + return pi.applicationInfo.enabled && isSystemApp; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "stats not found!"); + return false; + } + } + + /** + * Send an event to CyangenStats + * + * @param context {@link Context} not null + * @param trackingBundle {@link Bundle} + * @throws IllegalArgumentException + */ + public static void sendEvent(Context context, Bundle trackingBundle) + throws IllegalArgumentException { + if (context == null) { + throw new IllegalArgumentException("'context' cannot be null!"); + } + if (trackingBundle == null) { + throw new IllegalArgumentException("'trackingBundle' cannot be null!"); + } + if (!isStatsCollectionEnabled(context)) { + Logger.logd(TAG, "Stats collection: DISABLED!"); + return; + } + Logger.logd(TAG, "Stats collection: ENABLED!"); + + Intent newIntent = new Intent(ANALYTIC_INTENT); + + if (!trackingBundle.containsKey(KEY_TRACKING_ID)) { + Logger.logd(TAG, "No tracking id in bundle"); + return; + } else { + if (trackingBundle.containsKey(TrackingBundle.KEY_EVENT_CATEGORY) + && trackingBundle.containsKey(TrackingBundle.KEY_EVENT_ACTION)) { + Logger.logd(TAG, trackingBundle.toString()); + newIntent.putExtras(trackingBundle); + context.sendBroadcast(newIntent); + } else { + Logger.logd(TAG, "Not a valid tracking bundle"); + } + } + } + +} diff --git a/src/com/android/launcher3/stats/external/TrackingBundle.java b/src/com/android/launcher3/stats/external/TrackingBundle.java new file mode 100644 index 000000000..6ca5d971e --- /dev/null +++ b/src/com/android/launcher3/stats/external/TrackingBundle.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2015. The CyanogenMod 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.stats.external; + +import android.os.Bundle; +import android.text.TextUtils; + +/** + * <pre> + * Extension of a {@link Bundle} to provider streamline interfaces for + * the specific task of sending events + * </pre> + * + * @see {@link Bundle} + */ +public class TrackingBundle { + + // Constants + public static final String KEY_TRACKING_ID = "tracking_id"; + public static final String KEY_EVENT_CATEGORY = "category"; + public static final String KEY_EVENT_ACTION = "action"; + public static final String KEY_METADATA_VALUE = "value"; + public static final String KEY_METADATA_ORIGIN = "origin"; + public static final String KEY_METADATA_PACKAGE = "package"; + + + /** + * Constructor + * + * @param trackingId {@link String} + * @param category {@link String} + * @param action {@link String} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static Bundle createTrackingBundle(String trackingId, String category, String action) + throws IllegalArgumentException { + if (TextUtils.isEmpty(trackingId)) { + throw new IllegalArgumentException("'trackingId' cannot be null or empty!"); + } + if (TextUtils.isEmpty(category)) { + throw new IllegalArgumentException("'category' cannot be null or empty!"); + } + if (TextUtils.isEmpty(action)) { + throw new IllegalArgumentException("'action' cannot be null or empty!"); + } + Bundle bundle = new Bundle(); + bundle.putString(KEY_EVENT_CATEGORY, category); + bundle.putString(KEY_EVENT_ACTION, action); + bundle.putString(KEY_TRACKING_ID, trackingId); + return bundle; + } + +}
\ No newline at end of file diff --git a/src/com/android/launcher3/stats/internal/db/DatabaseHelper.java b/src/com/android/launcher3/stats/internal/db/DatabaseHelper.java new file mode 100644 index 000000000..7ffd509ff --- /dev/null +++ b/src/com/android/launcher3/stats/internal/db/DatabaseHelper.java @@ -0,0 +1,159 @@ +package com.android.launcher3.stats.internal.db; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import com.android.launcher3.stats.internal.model.TrackingEvent; +import com.android.launcher3.stats.util.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * <pre> + * Helper for accessing the database + * </pre> + * + * @see {@link SQLiteOpenHelper} + */ +public class DatabaseHelper extends SQLiteOpenHelper { + + // Constants + private static final String TAG = DatabaseHelper.class.getSimpleName(); + private static final String DATABASE_NAME = "events"; + private static final int DATABASE_VERSION = 1; + + // Instance + private static DatabaseHelper sInstance = null; + + /** + * Constructor + * + * @param context {@link Context} + * @return {@link DatabaseHelper} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static DatabaseHelper createInstance(Context context) throws IllegalArgumentException { + if (sInstance == null) { + sInstance = new DatabaseHelper(context); + } + return sInstance; + } + + /** + * Constructor + * + * @param context {@link Context} + */ + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + /** + * Write an event to the database + * + * @param trackingEvent {@link TrackingEvent} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public void writeEvent(TrackingEvent trackingEvent) + throws IllegalArgumentException { + if (trackingEvent == null) { + throw new IllegalArgumentException("'trackingEvent' cannot be null!"); + } + Logger.logd(TAG, "Event written to database: " + trackingEvent); + SQLiteDatabase db = getWritableDatabase(); + ContentValues contentValues = trackingEvent.toContentValues(); + db.insert(TrackingEventContract.EVENT_TABLE_NAME, null, contentValues); + db.close(); + } + + /** + * Get a list of tracking events + * + * @param instanceId {@link Integer} + * @return {@link List} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public List<TrackingEvent> getTrackingEventsByCategory(int instanceId, + TrackingEvent.Category category) throws IllegalArgumentException { + if (category == null) { + throw new IllegalArgumentException("'category' cannot be null!"); + } + + List<TrackingEvent> eventList = new ArrayList<TrackingEvent>(); + + // Get a writable database + SQLiteDatabase db = getWritableDatabase(); + + // Update unclaimed items for this instance + ContentValues contentValues = new ContentValues(); + contentValues.put(TrackingEventContract.EVENT_COLUMN_INSTANCE, instanceId); + String whereClause = TrackingEventContract.EVENT_COLUMN_INSTANCE + " IS NULL AND " + + TrackingEventContract.EVENT_COLUMN_CATEGORY + " = ? "; + String[] whereArgs = new String[] { + category.name(), + }; + int cnt = db.update(TrackingEventContract.EVENT_TABLE_NAME, contentValues, whereClause, + whereArgs); + + // Short circuit empty update + if (cnt < 1) { + return eventList; + } + + // Select all tagged items + String selection = TrackingEventContract.EVENT_COLUMN_CATEGORY + " = ? AND " + + TrackingEventContract.EVENT_COLUMN_INSTANCE + " = ? "; + String[] selectionArgs = new String[]{ + category.name(), + String.valueOf(instanceId), + }; + Cursor c = db.query(TrackingEventContract.EVENT_TABLE_NAME, null, selection, selectionArgs, + null, null, null); + + // Build return list + while (c != null && c.getCount() > 0 && c.moveToNext()) { + eventList.add(new TrackingEvent(c)); + } + + db.close(); + + return eventList; + } + + /** + * Deletes events related to the instance + * + * @param instanceId {@link Integer} + * @return {@link Integer} + */ + public int deleteEventsByInstanceId(int instanceId) { + SQLiteDatabase db = getWritableDatabase(); + String whereClause = TrackingEventContract.EVENT_COLUMN_INSTANCE + " = ?"; + String[] whereArgs = new String[]{ + String.valueOf(instanceId), + }; + int cnt = db.delete(TrackingEventContract.EVENT_TABLE_NAME, whereClause, whereArgs); + db.close(); + return cnt; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(TrackingEventContract.CREATE_EVENT_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + // [NOTE][MSB]: This will lose data, need to make sure this is handled if/when database + // schema changes + + // db.execSQL("DROP TABLE IF EXISTS " + TrackingEventContract.EVENT_TABLE_NAME); + // onCreate(db); + + } + +} diff --git a/src/com/android/launcher3/stats/internal/db/TrackingEventContract.java b/src/com/android/launcher3/stats/internal/db/TrackingEventContract.java new file mode 100644 index 000000000..481a43193 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/db/TrackingEventContract.java @@ -0,0 +1,31 @@ +package com.android.launcher3.stats.internal.db; + +import android.provider.BaseColumns; + +/** + * <pre> + * Table contract definition + * </pre> + * + * @see {@link BaseColumns} + */ +public class TrackingEventContract implements BaseColumns { + + // Constants + public static final String EVENT_TABLE_NAME = "event"; + + // Columns + public static final String EVENT_COLUMN_CATEGORY = "category"; + public static final String EVENT_COLUMN_METADATA = "metadata"; + public static final String EVENT_COLUMN_INSTANCE = "instance"; + + // SQL + public static final String CREATE_EVENT_TABLE = "CREATE TABLE " + EVENT_TABLE_NAME + + " ( " + + " `" + _ID + "` INTEGER PRIMARY KEY AUTOINCREMENT, " + + " `" + EVENT_COLUMN_CATEGORY + "` TEXT, " + + " `" + EVENT_COLUMN_METADATA + "` TEXT, " + + " `" + EVENT_COLUMN_INSTANCE + "` INTEGER " + + ");"; + +} diff --git a/src/com/android/launcher3/stats/internal/model/CountAction.java b/src/com/android/launcher3/stats/internal/model/CountAction.java new file mode 100644 index 000000000..d509d4d26 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/model/CountAction.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2015. The CyanogenMod 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.stats.internal.model; + +import android.os.Bundle; +import android.text.TextUtils; +import com.android.launcher3.stats.external.TrackingBundle; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * <pre> + * Handles the specific for sending a tracking event + * </pre> + * + * @see {@link ITrackingAction} + */ +public class CountAction implements ITrackingAction { + + public static final String TRACKING_ACTION = "count"; + + @Override + public String toString() { + return TRACKING_ACTION; + } + + @Override + public List<Bundle> createTrackingBundles(String trackingId, TrackingEvent.Category category, + List<TrackingEvent> eventList) { + + Map<String, List<TrackingEvent>> eventPackageMap = + new HashMap<String, List<TrackingEvent>>(); + + for (TrackingEvent event : eventList) { + String pkg = event.getMetaData(TrackingEvent.KEY_PACKAGE); + pkg = (TextUtils.isEmpty(pkg)) ? trackingId : pkg; + if (!eventPackageMap.containsKey(pkg)) { + eventPackageMap.put(pkg, new ArrayList<TrackingEvent>()); + } + eventPackageMap.get(pkg).add(event); + } + + List<Bundle> bundleList = new ArrayList<Bundle>(); + for (Map.Entry<String, List<TrackingEvent>> entry : eventPackageMap.entrySet()) { + Bundle bundle = TrackingBundle.createTrackingBundle(trackingId, category.name(), + TRACKING_ACTION); + bundle.putInt(TrackingBundle.KEY_METADATA_VALUE, entry.getValue().size()); + String pkg = entry.getKey(); + if (!pkg.equals(trackingId)) { + bundle.putString(TrackingBundle.KEY_METADATA_PACKAGE, pkg); + } + bundleList.add(bundle); + } + return bundleList; + } +} diff --git a/src/com/android/launcher3/stats/internal/model/CountOriginByPackageAction.java b/src/com/android/launcher3/stats/internal/model/CountOriginByPackageAction.java new file mode 100644 index 000000000..fc04ca088 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/model/CountOriginByPackageAction.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2015. The CyanogenMod 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.stats.internal.model; + +import android.os.Bundle; +import android.text.TextUtils; +import com.android.launcher3.stats.external.TrackingBundle; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * <pre> + * This is an action to send a count of events with common origins + * </pre> + */ +public class CountOriginByPackageAction implements ITrackingAction { + + public static final String TRACKING_ACTION = "count_by_origin"; + + @Override + public String toString() { + return TRACKING_ACTION; + } + + @Override + public List<Bundle> createTrackingBundles(String trackingId, TrackingEvent.Category category, + List<TrackingEvent> eventList) { + // Make an origin mapper + Map<String, Map<String, List<TrackingEvent>>> originEventMap = + new HashMap<String, Map<String, List<TrackingEvent>>>(); + + // Parse the event list and categorize by origin + for (TrackingEvent event : eventList) { + // We are parsing for things with origin, if no origin is set, discard it! + if (TextUtils.isEmpty(event.getMetaData(TrackingEvent.KEY_ORIGIN))) { + continue; + } + String originKey = event.getMetaData(TrackingEvent.KEY_ORIGIN); + if (!originEventMap.containsKey(originKey)) { + HashMap<String, List<TrackingEvent>> newMap = + new HashMap<String, List<TrackingEvent>>(); + originEventMap.put(originKey, newMap); + } + String packageName = event.getMetaData(TrackingEvent.KEY_PACKAGE); + // Set a default so our iteration picks it up and just discard package metadata + packageName = (TextUtils.isEmpty(packageName)) ? trackingId : packageName; + if (!originEventMap.get(originKey).containsKey(packageName)) { + originEventMap.get(originKey).put(packageName, new ArrayList<TrackingEvent>()); + } + originEventMap.get(originKey).get(packageName).add(event); + } + + // Start building result tracking bundles + List<Bundle> bundleList = new ArrayList<Bundle>(); + for (Map.Entry<String, Map<String, List<TrackingEvent>>> entry : + originEventMap.entrySet()) { + String origin = entry.getKey(); + for (Map.Entry<String, List<TrackingEvent>> entry2 : entry.getValue().entrySet()) { + String pkg = entry2.getKey(); + List<TrackingEvent> events = entry2.getValue(); + Bundle bundle = TrackingBundle.createTrackingBundle(trackingId, category.name(), + TRACKING_ACTION); + bundle.putString(TrackingBundle.KEY_METADATA_ORIGIN, origin); + bundle.putInt(TrackingBundle.KEY_METADATA_VALUE, events.size()); + if (!trackingId.equals(pkg)) { + bundle.putString(TrackingBundle.KEY_METADATA_PACKAGE, pkg); + } + bundleList.add(bundle); + } + } + return bundleList; + } + +} diff --git a/src/com/android/launcher3/stats/internal/model/ITrackingAction.java b/src/com/android/launcher3/stats/internal/model/ITrackingAction.java new file mode 100644 index 000000000..b577ed2d0 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/model/ITrackingAction.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2015. The CyanogenMod 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.stats.internal.model; + +import android.os.Bundle; + +import java.util.List; + +/** + * <pre> + * This is an action we want to perfrom from a report. + * + * e.g. + * 1. I want to get the COUNT of widgets added + * 2. I want to get the origin of app launches + * </pre> + */ +public interface ITrackingAction { + + /** + * Creates a new bundle used to tracking events + * + * @param trackingId {@link String} + * @param category {@link com.android.launcher3.stats.internal.model.TrackingEvent.Category} + * @param eventList {@link List} + * @return {@link List} + */ + List<Bundle> createTrackingBundles(String trackingId, TrackingEvent.Category category, + List<TrackingEvent> eventList); + +} diff --git a/src/com/android/launcher3/stats/internal/model/TrackingEvent.java b/src/com/android/launcher3/stats/internal/model/TrackingEvent.java new file mode 100644 index 000000000..552dac729 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/model/TrackingEvent.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2015. The CyanogenMod 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.stats.internal.model; + +import android.content.ContentValues; +import android.database.Cursor; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import com.android.launcher3.stats.external.TrackingBundle; +import com.android.launcher3.stats.internal.db.TrackingEventContract; +import com.android.launcher3.stats.util.Logger; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * <pre> + * Model of an event to track + * </pre> + */ +public class TrackingEvent { + + // Constants + private static final String TAG = TrackingEvent.class.getSimpleName(); + + // Members + private Category mCategory; + private final Map<String, String> mMetaData = new HashMap<String, String>(); + + public enum Category { + APP_LAUNCH, + WIDGET_ADD, + WIDGET_REMOVE, + SETTINGS_OPEN, + WALLPAPER_CHANGE, + HOMESCREEN_PAGE, + WIDGET, + + // Remote folder specific + REMOTE_FOLDER_DISABLED, + REMOTE_DRAWER_DISABLED, + REMOTE_FOLDER_OPENED, + REMOTE_FOLDER_INFO_OPENED, + REMOTE_APP_OPENED, + REMOTE_APP_INSTALLED, + REMOTE_SYNC_TIME, + REMOTE_SYNC_ERROR, + REMOTE_SYNC_UNFILLED, + REMOTE_FEATURE_DISABLED_FEEDBACK + } + + public static final String KEY_ORIGIN = TrackingBundle.KEY_METADATA_ORIGIN; + public static final String KEY_VALUE = TrackingBundle.KEY_METADATA_VALUE; + public static final String KEY_PACKAGE = TrackingBundle.KEY_METADATA_PACKAGE; + + /** + * Constructor + * + * @param category {@link TrackingEvent.Category} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public TrackingEvent(Category category) throws IllegalArgumentException { + if (category == null) { + throw new IllegalArgumentException("'category' cannot be null or empty!"); + } + mCategory = category; + } + + /** + * Constructor + * + * @param cursor {@link Cursor} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public TrackingEvent(Cursor cursor) throws IllegalArgumentException { + if (cursor == null) { + throw new IllegalArgumentException("'cursor' cannot be null!"); + } + mCategory = Category.valueOf(cursor.getString(cursor.getColumnIndex( + TrackingEventContract.EVENT_COLUMN_CATEGORY))); + String metadata = cursor.getString(cursor.getColumnIndex( + TrackingEventContract.EVENT_COLUMN_METADATA)); + if (!TextUtils.isEmpty(metadata)) { + String[] parts = metadata.split(","); + for (String part : parts) { + try { + String key = part.split("=")[0]; + String val = part.split("=")[1]; + mMetaData.put(key, val); + } catch (IndexOutOfBoundsException e) { + Log.w(TAG, e.getMessage(), e); + } + } + } + } + + /** + * Get the category + * + * @return {@link TrackingEvent.Category} + */ + public Category getCategory() { + return mCategory; + } + + /** + * Get the set of meta data keys + * + * @return {@link Set} + */ + public Set<String> getMetaDataKeySet() { + return mMetaData.keySet(); + } + + /** + * Set some meta data + * + * @param key {@link String} + * @param value {@link String} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public void setMetaData(String key, String value) throws IllegalArgumentException { + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("'key' cannot be null or empty!"); + } + if (TextUtils.isEmpty(value)) { + throw new IllegalArgumentException("'value' cannot be null or empty!"); + } + mMetaData.put(key, value); + } + + /** + * Get some meta data value + * + * @param key {@link String} + * @return {@link String} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public String getMetaData(String key) throws IllegalArgumentException { + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("'key' cannot be null or empty!"); + } + if (mMetaData.containsKey(key)) { + return mMetaData.get(key); + } + return null; + } + + /** + * Remove some meta data + * + * @param key {@link String} + * @return {@link String} or null + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public String removeMetaData(String key) throws IllegalArgumentException { + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("'key' cannot be null or empty!"); + } + if (mMetaData.containsKey(key)) { + return mMetaData.remove(key); + } + return null; + } + + /** + * Converts this object into content values for use with sqlite + * + * @return {@link ContentValues} + */ + public ContentValues toContentValues() { + ContentValues contentValues = new ContentValues(); + contentValues.put(TrackingEventContract.EVENT_COLUMN_CATEGORY, mCategory.name()); + StringBuilder sb = new StringBuilder(); + for (String key : mMetaData.keySet()) { + sb.append(key).append("=").append(mMetaData.get(key)).append(","); + } + if (sb.length() > 0) { + String metadata = sb.toString(); + metadata = metadata.substring(0, metadata.length() - 1); + Logger.logd(TAG, "MetaData: " + metadata); + contentValues.put(TrackingEventContract.EVENT_COLUMN_METADATA, metadata); + } + return contentValues; + } + + /** + * Convert this object into a tracking bundle + * + * @param trackingId {@link String} + * @param action {@link ITrackingAction} + * @return {@link Bundle} + */ + public Bundle toTrackingBundle(String trackingId, ITrackingAction action) { + Bundle bundle = TrackingBundle.createTrackingBundle(trackingId, mCategory.name(), + action.toString()); + return bundle; + } + +} diff --git a/src/com/android/launcher3/stats/internal/service/AggregationIntentService.java b/src/com/android/launcher3/stats/internal/service/AggregationIntentService.java new file mode 100644 index 000000000..cd9eaf793 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/service/AggregationIntentService.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2015. The CyanogenMod 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.stats.internal.service; + +import android.app.AlarmManager; +import android.app.IntentService; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherApplication; +import com.android.launcher3.stats.external.StatsUtil; +import com.android.launcher3.stats.external.TrackingBundle; +import com.android.launcher3.stats.internal.db.DatabaseHelper; +import com.android.launcher3.stats.internal.model.CountAction; +import com.android.launcher3.stats.internal.model.CountOriginByPackageAction; +import com.android.launcher3.stats.internal.model.ITrackingAction; +import com.android.launcher3.stats.internal.model.TrackingEvent; +import com.android.launcher3.stats.util.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * <pre> + * Service that starts on a timer and handles aggregating events and sending them to + * CyanogenStats + * </pre> + * + * @see {@link IntentService} + */ +public class AggregationIntentService extends IntentService { + + // Constants + private static final String TAG = AggregationIntentService.class.getSimpleName(); + private static final String TRACKING_ID = "com.cyanogenmod.trebuchet"; + public static final String ACTION_AGGREGATE_AND_TRACK = + "com.cyanogenmod.trebuchet.AGGREGATE_AND_TRACK"; + private static final List<ITrackingAction> TRACKED_ACTIONS = new ArrayList<ITrackingAction>() { + { + add(new CountAction()); + add(new CountOriginByPackageAction()); + } + }; + private static final int INVALID_COUNT = -1; + private static final String KEY_LAST_TIME_RAN = "last_time_stats_ran"; + public static final String PREF_KEY_PAGE_COUNT = "page_count"; + public static final String PREF_KEY_WIDGET_COUNT = "widget_count"; + + // Members + private DatabaseHelper mDatabaseHelper = null; + private int mInstanceId = -1; + private SharedPreferences mPrefs = null; + + /** + * Creates an IntentService. Invoked by your subclass's constructor. + */ + public AggregationIntentService() { + super(AggregationIntentService.class.getSimpleName()); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (!isTrebuchetDefaultLauncher()) { + // Cancel repeating schedule + unscheduleService(); + // don't return b/c we still want to upload whatever metrics are left. + } + String action = intent.getAction(); + if (ACTION_AGGREGATE_AND_TRACK.equals(action)) { + mPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), + Context.MODE_PRIVATE); + mPrefs.edit().putLong(KEY_LAST_TIME_RAN, System.currentTimeMillis()).apply(); + mInstanceId = (int) System.currentTimeMillis(); + mDatabaseHelper = DatabaseHelper.createInstance(this); + performAggregation(); + deleteTrackingEventsForInstance(); + handleNonEventMetrics(); + } + } + + private void performAggregation() { + + // Iterate available categories + for (TrackingEvent.Category category : TrackingEvent.Category.values()) { + + // Fetch the events from the database based on the category + List<TrackingEvent> eventList = + mDatabaseHelper.getTrackingEventsByCategory(mInstanceId, category); + + Logger.logd(TAG, "Event list size: " + eventList.size()); + // Short circuit if no events for the category + if (eventList.size() < 1) { + continue; + } + + // Now crunch the data into actionable events for the server + for (ITrackingAction action : TRACKED_ACTIONS) { + try { + for (Bundle bundle : action.createTrackingBundles(TRACKING_ID, category, + eventList)) { + performTrackingCall(bundle); + } + } catch (NullPointerException e) { + Log.e(TAG, "NPE fetching bundle list!", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Illegal argument!", e); + } + } + + } + } + + private void deleteTrackingEventsForInstance() { + mDatabaseHelper.deleteEventsByInstanceId(mInstanceId); + } + + /** + * These are metrics that are not event based and need a snapshot every INTERVAL + */ + private void handleNonEventMetrics() { + sendPageCountStats(); + sendWidgetCountStats(); + + } + + private void sendPageCountStats() { + int pageCount = mPrefs.getInt(PREF_KEY_PAGE_COUNT, INVALID_COUNT); + if (pageCount == INVALID_COUNT) { + return; + } + Bundle bundle = TrackingBundle + .createTrackingBundle(TRACKING_ID, TrackingEvent.Category.HOMESCREEN_PAGE.name(), + "count"); + bundle.putString(TrackingEvent.KEY_VALUE, String.valueOf(pageCount)); + StatsUtil.sendEvent(this, bundle); + } + + private void sendWidgetCountStats() { + int widgetCount = mPrefs.getInt(PREF_KEY_WIDGET_COUNT, INVALID_COUNT); + if (widgetCount == INVALID_COUNT) { + return; + } + Bundle bundle = TrackingBundle + .createTrackingBundle(TRACKING_ID, TrackingEvent.Category.WIDGET.name(), "count"); + bundle.putString(TrackingEvent.KEY_VALUE, String.valueOf(widgetCount)); + StatsUtil.sendEvent(this, bundle); + } + + private void performTrackingCall(Bundle bundle) throws IllegalArgumentException { + StatsUtil.sendEvent(this, bundle); + } + + private void unscheduleService() { + Intent intent = new Intent(this, AggregationIntentService.class); + intent.setAction(ACTION_AGGREGATE_AND_TRACK); + PendingIntent pi = PendingIntent.getService(this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + AlarmManager alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); + alarmManager.cancel(pi); + } + + private boolean isTrebuchetDefaultLauncher() { + final IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN); + filter.addCategory(Intent.CATEGORY_HOME); + + List<IntentFilter> filters = new ArrayList<IntentFilter>(); + filters.add(filter); + + final String myPackageName = getPackageName(); + List<ComponentName> activities = new ArrayList<ComponentName>(); + final PackageManager packageManager = getPackageManager(); + + // You can use name of your package here as third argument + packageManager.getPreferredActivities(filters, activities, null); + + for (ComponentName activity : activities) { + if (myPackageName.equals(activity.getPackageName())) { + Logger.logd(TAG, "Trebuchet IS default launcher!"); + return true; + } + } + Logger.logd(TAG, "Trebuchet IS NOT default launcher!"); + return false; + } + + private static final long ALARM_INTERVAL = 86400000; // 1 day + + /** + * Schedule an alarm service, will cancel existing + * + * @param context {@link Context} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static void scheduleService(Context context) throws IllegalArgumentException { + if (context == null) { + throw new IllegalArgumentException("'context' cannot be null!"); + } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + long lastTimeRan = prefs.getLong(KEY_LAST_TIME_RAN, 0); + Intent intent = new Intent(context, AggregationIntentService.class); + intent.setAction(ACTION_AGGREGATE_AND_TRACK); + PendingIntent pi = PendingIntent.getService(context, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + alarmManager.cancel(pi); + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, lastTimeRan + ALARM_INTERVAL, + ALARM_INTERVAL, pi); + } + +} diff --git a/src/com/android/launcher3/stats/util/Logger.java b/src/com/android/launcher3/stats/util/Logger.java new file mode 100644 index 000000000..8d73f54ca --- /dev/null +++ b/src/com/android/launcher3/stats/util/Logger.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015. The CyanogenMod 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.stats.util; + +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +/** + * <pre> + * Metrics debug logging + * </pre> + */ +public class Logger { + + private static final String TAG = "TrebuchetStats"; + + /** + * Log a debug message + * + * @param tag {@link String} + * @param msg {@link String } + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static void logd(String tag, String msg) throws IllegalArgumentException { + if (TextUtils.isEmpty(tag)) { + throw new IllegalArgumentException("'tag' cannot be empty!"); + } + if (TextUtils.isEmpty(msg)) { + throw new IllegalArgumentException("'msg' cannot be empty!"); + } + if (isDebugging()) { + Log.d(TAG, tag + " [ " + msg + " ]"); + } + } + + private static boolean isDebugging() { + return Log.isLoggable(TAG, Log.DEBUG); + } + +} diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java index 0c6ea31bb..fff60a1d5 100644 --- a/src/com/android/launcher3/widget/WidgetsContainerView.java +++ b/src/com/android/launcher3/widget/WidgetsContainerView.java @@ -29,6 +29,7 @@ import android.view.View; import android.widget.Toast; import com.android.launcher3.BaseContainerView; +import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DeviceProfile; @@ -114,6 +115,19 @@ public class WidgetsContainerView extends BaseContainerView }); mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); + setScroller(); + updateBackgroundAndPaddings(); + } + + public void reset() { + updateScrubber(); + updateBackgroundAndPaddings(); + } + + private void updateScrubber() { + if (useScroller() && useScrubber()) { + mScrubber.updateSections(); + } } // @@ -124,6 +138,12 @@ public class WidgetsContainerView extends BaseContainerView return mView; } + public void setScrubberVisibility(int visibility) { + if (mScrubberContainerView != null) { + mScrubberContainerView.setVisibility(visibility); + } + } + public View getRevealView() { // TODO(hyunyoungs): temporarily use apps view transition. return findViewById(R.id.widgets_reveal_view); @@ -336,6 +356,7 @@ public class WidgetsContainerView extends BaseContainerView @Override protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) { + boolean isRtl = Utilities.isRtl(getResources()); // Apply the top-bottom padding to the content itself so that the launcher transition is // clipped correctly mContent.setPadding(0, padding.top, 0, padding.bottom); @@ -349,6 +370,27 @@ public class WidgetsContainerView extends BaseContainerView mView.setBackground(background); getRevealView().setBackground(background.getConstantState().newDrawable()); mView.updateBackgroundPadding(bgPadding); + + int startInset = mView.getMaxScrollbarWidth(); + int topBottomPadding = getPaddingTop(); + final boolean useScrollerScrubber = useScroller() && useScrubber(); + if (isRtl) { + mView.setPadding(padding.left + mView.getMaxScrollbarWidth(), + topBottomPadding, padding.right + startInset, useScrollerScrubber ? + mScrubberHeight + topBottomPadding : topBottomPadding); + if (useScrollerScrubber) { + mScrubberContainerView.setPadding(padding.left, 0, padding.right, 0); + } + } else { + mView.setPadding(padding.left + startInset, topBottomPadding, + padding.right + mView.getMaxScrollbarWidth(), useScrollerScrubber ? + mScrubberHeight + topBottomPadding : topBottomPadding); + if (useScrollerScrubber) { + mScrubberContainerView.setPadding(padding.left, 0, padding.right, 0); + mScrubberContainerView.setEnabled(true); + mScrubberContainerView.bringToFront(); + } + } } /** @@ -358,6 +400,14 @@ public class WidgetsContainerView extends BaseContainerView mView.setWidgets(model); mAdapter.setWidgetsModel(model); mAdapter.notifyDataSetChanged(); + updateScrubber(); + } + + public WidgetsModel getWidgets() { + if (mView != null) { + return mView.getWidgets(); + } + return null; } private WidgetPreviewLoader getWidgetPreviewLoader() { @@ -366,4 +416,9 @@ public class WidgetsContainerView extends BaseContainerView } return mWidgetPreviewLoader; } + + @Override + protected BaseRecyclerView getRecyclerView() { + return mView; + } }
\ No newline at end of file diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java index 884bdc418..6818f3f71 100644 --- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java +++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java @@ -20,13 +20,17 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.support.v7.widget.LinearLayoutManager; +import android.text.TextUtils; import android.util.AttributeSet; +import android.util.Log; import android.view.View; import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.R; import com.android.launcher3.model.PackageItemInfo; import com.android.launcher3.model.WidgetsModel; +import java.util.ArrayList; + /** * The widgets recycler view. */ @@ -70,7 +74,11 @@ public class WidgetsRecyclerView extends BaseRecyclerView { public void setWidgets(WidgetsModel widgets) { mWidgets = widgets; } - + + public WidgetsModel getWidgets() { + return mWidgets; + } + /** * We need to override the draw to ensure that we don't draw the overscroll effect beyond the * background bounds. @@ -126,20 +134,66 @@ public class WidgetsRecyclerView extends BaseRecyclerView { // Skip early if there are no widgets. int rowCount = mWidgets.getPackageSize(); if (rowCount == 0) { - mScrollbar.setThumbOffset(-1, -1); + if (mUseScrollbar) { + mScrollbar.setThumbOffset(-1, -1); + } return; } // Skip early if, there no child laid out in the container. getCurScrollState(mScrollPosState); if (mScrollPosState.rowIndex < 0) { - mScrollbar.setThumbOffset(-1, -1); + if (mUseScrollbar) { + mScrollbar.setThumbOffset(-1, -1); + } return; } synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount); } + @Override + public String scrollToSection(String sectionName) { + // Skip early if widgets are not bound. + if (mWidgets == null) { + return ""; + } + + // Skip early if there are no widgets. + int rowCount = mWidgets.getPackageSize(); + if (rowCount == 0) { + return ""; + } + for (int i = 0; i < rowCount; i++) { + PackageItemInfo packageItemInfo = mWidgets.getPackageItemInfo(i); + if (packageItemInfo != null && !TextUtils.isEmpty(packageItemInfo.titleSectionName) && + packageItemInfo.titleSectionName.equals(sectionName)) { + LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); + layoutManager.smoothScrollToPosition(this, null, i); + return packageItemInfo.titleSectionName; + } + } + return null; + } + + @Override + public String[] getSectionNames() { + if (mWidgets == null) { + return new String[0]; + } + final int N = mWidgets.getPackageSize(); + ArrayList<String> sections = new ArrayList<>(); + String lastLetter = null; + for (int i = 0; i < N; i++) { + final String titleSectionName = mWidgets.getPackageItemInfo(i).titleSectionName; + if (!TextUtils.isEmpty(titleSectionName) && !titleSectionName.equals(lastLetter)) { + lastLetter = titleSectionName; + sections.add(titleSectionName); + } + } + return sections.toArray(new String[sections.size()]); + } + /** * Returns the current scroll state. */ |