/* * 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.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 Context mContext; @ViewDebug.ExportedProperty(category = "launcher") private int mPreviousOrientation; 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); mContext = context; mLongPressHelper = new CheckLongPressHelper(this, this); mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this); mInflater = LayoutInflater.from(context); setAccessibilityDelegate(Launcher.getLauncher(context).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); } public void updateLastInflationOrientation() { mPreviousOrientation = mContext.getResources().getConfiguration().orientation; } @Override public void updateAppWidget(RemoteViews remoteViews) { // Store the orientation in which the widget was inflated updateLastInflationOrientation(); super.updateAppWidget(remoteViews); // The provider info or the views might have changed. checkIfAutoAdvance(); } 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 isReinflateRequired(int orientation) { // Re-inflate is required if the orientation has changed since last inflated. if (mPreviousOrientation != orientation) { 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 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; } }