blob: 6f953e5f0a999c290f045fae6623824b3e058afa [file] [log] [blame]
/*
* 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();
}
}
}
}