| /* |
| * Copyright (C) 2009 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.appwidget.AppWidgetHostView; |
| import android.appwidget.AppWidgetProviderInfo; |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.graphics.PointF; |
| import android.graphics.Rect; |
| import android.os.Handler; |
| import android.os.SystemClock; |
| import android.util.SparseBooleanArray; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewDebug; |
| import android.view.ViewGroup; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.widget.AdapterView; |
| import android.widget.Advanceable; |
| import android.widget.RemoteViews; |
| |
| import com.android.launcher3.dragndrop.DragLayer; |
| import com.android.launcher3.dragndrop.DragLayer.TouchCompleteListener; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public class LauncherAppWidgetHostView extends AppWidgetHostView |
| implements TouchCompleteListener, View.OnLongClickListener { |
| |
| // Related to the auto-advancing of widgets |
| private static final long ADVANCE_INTERVAL = 20000; |
| private static final long ADVANCE_STAGGER = 250; |
| |
| // Maintains a list of widget ids which are supposed to be auto advanced. |
| private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray(); |
| |
| protected final LayoutInflater mInflater; |
| |
| private final CheckLongPressHelper mLongPressHelper; |
| private final StylusEventHelper mStylusEventHelper; |
| private final Launcher mLauncher; |
| |
| private static final int DONT_REINFLATE = 0; |
| private static final int REINFLATE_ON_RESUME = 1; |
| private static final int REINFLATE_ON_CONFIG_CHANGE = 2; |
| |
| @ViewDebug.ExportedProperty(category = "launcher") |
| private int mReinflateStatus; |
| |
| private float mSlop; |
| |
| @ViewDebug.ExportedProperty(category = "launcher") |
| private boolean mChildrenFocused; |
| |
| private boolean mIsScrollable; |
| private boolean mIsAttachedToWindow; |
| private boolean mIsAutoAdvanceRegistered; |
| private Runnable mAutoAdvanceRunnable; |
| |
| /** |
| * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY. |
| */ |
| private float mScaleToFit = 1f; |
| |
| /** |
| * The translation values to center the widget within its cellspans. |
| */ |
| private final PointF mTranslationForCentering = new PointF(0, 0); |
| |
| public LauncherAppWidgetHostView(Context context) { |
| super(context); |
| mLauncher = Launcher.getLauncher(context); |
| mLongPressHelper = new CheckLongPressHelper(this, this); |
| mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this); |
| mInflater = LayoutInflater.from(context); |
| setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); |
| setBackgroundResource(R.drawable.widget_internal_focus_bg); |
| |
| if (Utilities.ATLEAST_OREO) { |
| setExecutor(Utilities.THREAD_POOL_EXECUTOR); |
| } |
| } |
| |
| @Override |
| public boolean onLongClick(View view) { |
| if (mIsScrollable) { |
| DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer(); |
| dragLayer.requestDisallowInterceptTouchEvent(false); |
| } |
| view.performLongClick(); |
| return true; |
| } |
| |
| @Override |
| protected View getErrorView() { |
| return mInflater.inflate(R.layout.appwidget_error, this, false); |
| } |
| |
| @Override |
| public void updateAppWidget(RemoteViews remoteViews) { |
| super.updateAppWidget(remoteViews); |
| |
| // The provider info or the views might have changed. |
| checkIfAutoAdvance(); |
| |
| // It is possible that widgets can receive updates while launcher is not in the foreground. |
| // Consequently, the widgets will be inflated for the orientation of the foreground activity |
| // (framework issue). On resuming, we ensure that any widgets are inflated for the current |
| // orientation. |
| if (mReinflateStatus == DONT_REINFLATE && !isSameOrientation()) { |
| mReinflateStatus = REINFLATE_ON_RESUME; |
| if (!mLauncher.waitUntilResume(new ReInflateRunnable())) { |
| mReinflateStatus = REINFLATE_ON_CONFIG_CHANGE; |
| } |
| } |
| } |
| |
| private boolean isSameOrientation() { |
| return mLauncher.getResources().getConfiguration().orientation == |
| mLauncher.getOrientation(); |
| } |
| |
| private boolean checkScrollableRecursively(ViewGroup viewGroup) { |
| if (viewGroup instanceof AdapterView) { |
| return true; |
| } else { |
| for (int i=0; i < viewGroup.getChildCount(); i++) { |
| View child = viewGroup.getChildAt(i); |
| if (child instanceof ViewGroup) { |
| if (checkScrollableRecursively((ViewGroup) child)) { |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| // Just in case the previous long press hasn't been cleared, we make sure to start fresh |
| // on touch down. |
| if (ev.getAction() == MotionEvent.ACTION_DOWN) { |
| mLongPressHelper.cancelLongPress(); |
| } |
| |
| // Consume any touch events for ourselves after longpress is triggered |
| if (mLongPressHelper.hasPerformedLongPress()) { |
| mLongPressHelper.cancelLongPress(); |
| return true; |
| } |
| |
| // Watch for longpress or stylus button press events at this level to |
| // make sure users can always pick up this widget |
| if (mStylusEventHelper.onMotionEvent(ev)) { |
| mLongPressHelper.cancelLongPress(); |
| return true; |
| } |
| |
| switch (ev.getAction()) { |
| case MotionEvent.ACTION_DOWN: { |
| DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer(); |
| |
| if (mIsScrollable) { |
| dragLayer.requestDisallowInterceptTouchEvent(true); |
| } |
| if (!mStylusEventHelper.inStylusButtonPressed()) { |
| mLongPressHelper.postCheckForLongPress(); |
| } |
| dragLayer.setTouchCompleteListener(this); |
| break; |
| } |
| |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| mLongPressHelper.cancelLongPress(); |
| break; |
| case MotionEvent.ACTION_MOVE: |
| if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) { |
| mLongPressHelper.cancelLongPress(); |
| } |
| break; |
| } |
| |
| // Otherwise continue letting touch events fall through to children |
| return false; |
| } |
| |
| public boolean onTouchEvent(MotionEvent ev) { |
| // If the widget does not handle touch, then cancel |
| // long press when we release the touch |
| switch (ev.getAction()) { |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| mLongPressHelper.cancelLongPress(); |
| break; |
| case MotionEvent.ACTION_MOVE: |
| if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) { |
| mLongPressHelper.cancelLongPress(); |
| } |
| break; |
| } |
| return false; |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); |
| |
| mIsAttachedToWindow = true; |
| checkIfAutoAdvance(); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| |
| // We can't directly use isAttachedToWindow() here, as this is called before the internal |
| // state is updated. So isAttachedToWindow() will return true until next frame. |
| mIsAttachedToWindow = false; |
| checkIfAutoAdvance(); |
| } |
| |
| @Override |
| public void cancelLongPress() { |
| super.cancelLongPress(); |
| mLongPressHelper.cancelLongPress(); |
| } |
| |
| @Override |
| public AppWidgetProviderInfo getAppWidgetInfo() { |
| AppWidgetProviderInfo info = super.getAppWidgetInfo(); |
| if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) { |
| throw new IllegalStateException("Launcher widget must have" |
| + " LauncherAppWidgetProviderInfo"); |
| } |
| return info; |
| } |
| |
| @Override |
| public void onTouchComplete() { |
| if (!mLongPressHelper.hasPerformedLongPress()) { |
| // If a long press has been performed, we don't want to clear the record of that since |
| // we still may be receiving a touch up which we want to intercept |
| mLongPressHelper.cancelLongPress(); |
| } |
| } |
| |
| @Override |
| public int getDescendantFocusability() { |
| return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS |
| : ViewGroup.FOCUS_BLOCK_DESCENDANTS; |
| } |
| |
| @Override |
| public boolean dispatchKeyEvent(KeyEvent event) { |
| if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE |
| && event.getAction() == KeyEvent.ACTION_UP) { |
| mChildrenFocused = false; |
| requestFocus(); |
| return true; |
| } |
| return super.dispatchKeyEvent(event); |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { |
| event.startTracking(); |
| return true; |
| } |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| if (event.isTracking()) { |
| if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { |
| mChildrenFocused = true; |
| ArrayList<View> focusableChildren = getFocusables(FOCUS_FORWARD); |
| focusableChildren.remove(this); |
| int childrenCount = focusableChildren.size(); |
| switch (childrenCount) { |
| case 0: |
| mChildrenFocused = false; |
| break; |
| case 1: { |
| if (getTag() instanceof ItemInfo) { |
| ItemInfo item = (ItemInfo) getTag(); |
| if (item.spanX == 1 && item.spanY == 1) { |
| focusableChildren.get(0).performClick(); |
| mChildrenFocused = false; |
| return true; |
| } |
| } |
| // continue; |
| } |
| default: |
| focusableChildren.get(0).requestFocus(); |
| return true; |
| } |
| } |
| } |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| @Override |
| protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { |
| if (gainFocus) { |
| mChildrenFocused = false; |
| dispatchChildFocus(false); |
| } |
| super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); |
| } |
| |
| @Override |
| public void requestChildFocus(View child, View focused) { |
| super.requestChildFocus(child, focused); |
| dispatchChildFocus(mChildrenFocused && focused != null); |
| if (focused != null) { |
| focused.setFocusableInTouchMode(false); |
| } |
| } |
| |
| @Override |
| public void clearChildFocus(View child) { |
| super.clearChildFocus(child); |
| dispatchChildFocus(false); |
| } |
| |
| @Override |
| public boolean dispatchUnhandledMove(View focused, int direction) { |
| return mChildrenFocused; |
| } |
| |
| private void dispatchChildFocus(boolean childIsFocused) { |
| // The host view's background changes when selected, to indicate the focus is inside. |
| setSelected(childIsFocused); |
| } |
| |
| public void switchToErrorView() { |
| // Update the widget with 0 Layout id, to reset the view to error view. |
| updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0)); |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| try { |
| super.onLayout(changed, left, top, right, bottom); |
| } catch (final RuntimeException e) { |
| post(new Runnable() { |
| @Override |
| public void run() { |
| switchToErrorView(); |
| } |
| }); |
| } |
| |
| mIsScrollable = checkScrollableRecursively(this); |
| } |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(info); |
| info.setClassName(getClass().getName()); |
| } |
| |
| @Override |
| protected void onWindowVisibilityChanged(int visibility) { |
| super.onWindowVisibilityChanged(visibility); |
| maybeRegisterAutoAdvance(); |
| } |
| |
| private void checkIfAutoAdvance() { |
| boolean isAutoAdvance = false; |
| Advanceable target = getAdvanceable(); |
| if (target != null) { |
| isAutoAdvance = true; |
| target.fyiWillBeAdvancedByHostKThx(); |
| } |
| |
| boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0; |
| if (isAutoAdvance != wasAutoAdvance) { |
| if (isAutoAdvance) { |
| sAutoAdvanceWidgetIds.put(getAppWidgetId(), true); |
| } else { |
| sAutoAdvanceWidgetIds.delete(getAppWidgetId()); |
| } |
| maybeRegisterAutoAdvance(); |
| } |
| } |
| |
| private Advanceable getAdvanceable() { |
| AppWidgetProviderInfo info = getAppWidgetInfo(); |
| if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) { |
| return null; |
| } |
| View v = findViewById(info.autoAdvanceViewId); |
| return (v instanceof Advanceable) ? (Advanceable) v : null; |
| } |
| |
| private void maybeRegisterAutoAdvance() { |
| Handler handler = getHandler(); |
| boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null |
| && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0); |
| if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) { |
| mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance; |
| if (mAutoAdvanceRunnable == null) { |
| mAutoAdvanceRunnable = new Runnable() { |
| @Override |
| public void run() { |
| runAutoAdvance(); |
| } |
| }; |
| } |
| |
| handler.removeCallbacks(mAutoAdvanceRunnable); |
| scheduleNextAdvance(); |
| } |
| } |
| |
| private void scheduleNextAdvance() { |
| if (!mIsAutoAdvanceRegistered) { |
| return; |
| } |
| long now = SystemClock.uptimeMillis(); |
| long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) + |
| ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()); |
| Handler handler = getHandler(); |
| if (handler != null) { |
| handler.postAtTime(mAutoAdvanceRunnable, advanceTime); |
| } |
| } |
| |
| private void runAutoAdvance() { |
| Advanceable target = getAdvanceable(); |
| if (target != null) { |
| target.advance(); |
| } |
| scheduleNextAdvance(); |
| } |
| |
| public void setScaleToFit(float scale) { |
| mScaleToFit = scale; |
| setScaleX(scale); |
| setScaleY(scale); |
| } |
| |
| public float getScaleToFit() { |
| return mScaleToFit; |
| } |
| |
| public void setTranslationForCentering(float x, float y) { |
| mTranslationForCentering.set(x, y); |
| setTranslationX(x); |
| setTranslationY(y); |
| } |
| |
| public PointF getTranslationForCentering() { |
| return mTranslationForCentering; |
| } |
| |
| @Override |
| protected void onConfigurationChanged(Configuration newConfig) { |
| super.onConfigurationChanged(newConfig); |
| |
| if (mReinflateStatus == REINFLATE_ON_CONFIG_CHANGE) { |
| // We are finally in the same orientation |
| reinflateIfNecessary(); |
| } |
| } |
| |
| private void reinflateIfNecessary() { |
| if (!isSameOrientation()) { |
| // We cannot reinflate yet, wait until next config change |
| mReinflateStatus = REINFLATE_ON_CONFIG_CHANGE; |
| return; |
| } |
| |
| mReinflateStatus = DONT_REINFLATE; |
| if (isAttachedToWindow()) { |
| LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); |
| reinflate(); |
| } |
| } |
| |
| public void reinflate() { |
| LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); |
| // Remove and rebind the current widget (which was inflated in the wrong |
| // orientation), but don't delete it from the database |
| mLauncher.removeItem(this, info, false /* deleteFromDb */); |
| mLauncher.bindAppWidget(info); |
| } |
| |
| private class ReInflateRunnable implements Runnable { |
| @Override |
| public void run() { |
| if (mReinflateStatus == REINFLATE_ON_RESUME) { |
| reinflateIfNecessary(); |
| } |
| } |
| } |
| } |