1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3; 18 19 import android.appwidget.AppWidgetHostView; 20 import android.appwidget.AppWidgetProviderInfo; 21 import android.content.Context; 22 import android.graphics.PointF; 23 import android.graphics.Rect; 24 import android.os.Handler; 25 import android.os.SystemClock; 26 import android.util.SparseBooleanArray; 27 import android.view.KeyEvent; 28 import android.view.LayoutInflater; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewConfiguration; 32 import android.view.ViewDebug; 33 import android.view.ViewGroup; 34 import android.view.accessibility.AccessibilityNodeInfo; 35 import android.widget.AdapterView; 36 import android.widget.Advanceable; 37 import android.widget.RemoteViews; 38 39 import com.android.launcher3.dragndrop.DragLayer; 40 import com.android.launcher3.dragndrop.DragLayer.TouchCompleteListener; 41 42 import java.util.ArrayList; 43 44 /** 45 * {@inheritDoc} 46 */ 47 public class LauncherAppWidgetHostView extends AppWidgetHostView 48 implements TouchCompleteListener, View.OnLongClickListener { 49 50 // Related to the auto-advancing of widgets 51 private static final long ADVANCE_INTERVAL = 20000; 52 private static final long ADVANCE_STAGGER = 250; 53 54 // Maintains a list of widget ids which are supposed to be auto advanced. 55 private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray(); 56 57 protected final LayoutInflater mInflater; 58 59 private final CheckLongPressHelper mLongPressHelper; 60 private final StylusEventHelper mStylusEventHelper; 61 private final Context mContext; 62 63 @ViewDebug.ExportedProperty(category = "launcher") 64 private int mPreviousOrientation; 65 66 private float mSlop; 67 68 @ViewDebug.ExportedProperty(category = "launcher") 69 private boolean mChildrenFocused; 70 71 private boolean mIsScrollable; 72 private boolean mIsAttachedToWindow; 73 private boolean mIsAutoAdvanceRegistered; 74 private Runnable mAutoAdvanceRunnable; 75 76 /** 77 * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY. 78 */ 79 private float mScaleToFit = 1f; 80 81 /** 82 * The translation values to center the widget within its cellspans. 83 */ 84 private final PointF mTranslationForCentering = new PointF(0, 0); 85 86 public LauncherAppWidgetHostView(Context context) { 87 super(context); 88 mContext = context; 89 mLongPressHelper = new CheckLongPressHelper(this, this); 90 mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this); 91 mInflater = LayoutInflater.from(context); 92 setAccessibilityDelegate(Launcher.getLauncher(context).getAccessibilityDelegate()); 93 setBackgroundResource(R.drawable.widget_internal_focus_bg); 94 95 if (Utilities.ATLEAST_OREO) { 96 setExecutor(Utilities.THREAD_POOL_EXECUTOR); 97 } 98 } 99 100 @Override 101 public boolean onLongClick(View view) { 102 if (mIsScrollable) { 103 DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer(); 104 dragLayer.requestDisallowInterceptTouchEvent(false); 105 } 106 view.performLongClick(); 107 return true; 108 } 109 110 @Override 111 protected View getErrorView() { 112 return mInflater.inflate(R.layout.appwidget_error, this, false); 113 } 114 115 public void updateLastInflationOrientation() { 116 mPreviousOrientation = mContext.getResources().getConfiguration().orientation; 117 } 118 119 @Override 120 public void updateAppWidget(RemoteViews remoteViews) { 121 // Store the orientation in which the widget was inflated 122 updateLastInflationOrientation(); 123 super.updateAppWidget(remoteViews); 124 125 // The provider info or the views might have changed. 126 checkIfAutoAdvance(); 127 } 128 129 private boolean checkScrollableRecursively(ViewGroup viewGroup) { 130 if (viewGroup instanceof AdapterView) { 131 return true; 132 } else { 133 for (int i=0; i < viewGroup.getChildCount(); i++) { 134 View child = viewGroup.getChildAt(i); 135 if (child instanceof ViewGroup) { 136 if (checkScrollableRecursively((ViewGroup) child)) { 137 return true; 138 } 139 } 140 } 141 } 142 return false; 143 } 144 145 public boolean isReinflateRequired(int orientation) { 146 // Re-inflate is required if the orientation has changed since last inflated. 147 if (mPreviousOrientation != orientation) { 148 return true; 149 } 150 return false; 151 } 152 153 public boolean onInterceptTouchEvent(MotionEvent ev) { 154 // Just in case the previous long press hasn't been cleared, we make sure to start fresh 155 // on touch down. 156 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 157 mLongPressHelper.cancelLongPress(); 158 } 159 160 // Consume any touch events for ourselves after longpress is triggered 161 if (mLongPressHelper.hasPerformedLongPress()) { 162 mLongPressHelper.cancelLongPress(); 163 return true; 164 } 165 166 // Watch for longpress or stylus button press events at this level to 167 // make sure users can always pick up this widget 168 if (mStylusEventHelper.onMotionEvent(ev)) { 169 mLongPressHelper.cancelLongPress(); 170 return true; 171 } 172 173 switch (ev.getAction()) { 174 case MotionEvent.ACTION_DOWN: { 175 DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer(); 176 177 if (mIsScrollable) { 178 dragLayer.requestDisallowInterceptTouchEvent(true); 179 } 180 if (!mStylusEventHelper.inStylusButtonPressed()) { 181 mLongPressHelper.postCheckForLongPress(); 182 } 183 dragLayer.setTouchCompleteListener(this); 184 break; 185 } 186 187 case MotionEvent.ACTION_UP: 188 case MotionEvent.ACTION_CANCEL: 189 mLongPressHelper.cancelLongPress(); 190 break; 191 case MotionEvent.ACTION_MOVE: 192 if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) { 193 mLongPressHelper.cancelLongPress(); 194 } 195 break; 196 } 197 198 // Otherwise continue letting touch events fall through to children 199 return false; 200 } 201 202 public boolean onTouchEvent(MotionEvent ev) { 203 // If the widget does not handle touch, then cancel 204 // long press when we release the touch 205 switch (ev.getAction()) { 206 case MotionEvent.ACTION_UP: 207 case MotionEvent.ACTION_CANCEL: 208 mLongPressHelper.cancelLongPress(); 209 break; 210 case MotionEvent.ACTION_MOVE: 211 if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) { 212 mLongPressHelper.cancelLongPress(); 213 } 214 break; 215 } 216 return false; 217 } 218 219 @Override 220 protected void onAttachedToWindow() { 221 super.onAttachedToWindow(); 222 mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 223 224 mIsAttachedToWindow = true; 225 checkIfAutoAdvance(); 226 } 227 228 @Override 229 protected void onDetachedFromWindow() { 230 super.onDetachedFromWindow(); 231 232 // We can't directly use isAttachedToWindow() here, as this is called before the internal 233 // state is updated. So isAttachedToWindow() will return true until next frame. 234 mIsAttachedToWindow = false; 235 checkIfAutoAdvance(); 236 } 237 238 @Override 239 public void cancelLongPress() { 240 super.cancelLongPress(); 241 mLongPressHelper.cancelLongPress(); 242 } 243 244 @Override 245 public AppWidgetProviderInfo getAppWidgetInfo() { 246 AppWidgetProviderInfo info = super.getAppWidgetInfo(); 247 if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) { 248 throw new IllegalStateException("Launcher widget must have" 249 + " LauncherAppWidgetProviderInfo"); 250 } 251 return info; 252 } 253 254 @Override 255 public void onTouchComplete() { 256 if (!mLongPressHelper.hasPerformedLongPress()) { 257 // If a long press has been performed, we don't want to clear the record of that since 258 // we still may be receiving a touch up which we want to intercept 259 mLongPressHelper.cancelLongPress(); 260 } 261 } 262 263 @Override 264 public int getDescendantFocusability() { 265 return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS 266 : ViewGroup.FOCUS_BLOCK_DESCENDANTS; 267 } 268 269 @Override 270 public boolean dispatchKeyEvent(KeyEvent event) { 271 if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE 272 && event.getAction() == KeyEvent.ACTION_UP) { 273 mChildrenFocused = false; 274 requestFocus(); 275 return true; 276 } 277 return super.dispatchKeyEvent(event); 278 } 279 280 @Override 281 public boolean onKeyDown(int keyCode, KeyEvent event) { 282 if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { 283 event.startTracking(); 284 return true; 285 } 286 return super.onKeyDown(keyCode, event); 287 } 288 289 @Override 290 public boolean onKeyUp(int keyCode, KeyEvent event) { 291 if (event.isTracking()) { 292 if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { 293 mChildrenFocused = true; 294 ArrayList<View> focusableChildren = getFocusables(FOCUS_FORWARD); 295 focusableChildren.remove(this); 296 int childrenCount = focusableChildren.size(); 297 switch (childrenCount) { 298 case 0: 299 mChildrenFocused = false; 300 break; 301 case 1: { 302 if (getTag() instanceof ItemInfo) { 303 ItemInfo item = (ItemInfo) getTag(); 304 if (item.spanX == 1 && item.spanY == 1) { 305 focusableChildren.get(0).performClick(); 306 mChildrenFocused = false; 307 return true; 308 } 309 } 310 // continue; 311 } 312 default: 313 focusableChildren.get(0).requestFocus(); 314 return true; 315 } 316 } 317 } 318 return super.onKeyUp(keyCode, event); 319 } 320 321 @Override 322 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 323 if (gainFocus) { 324 mChildrenFocused = false; 325 dispatchChildFocus(false); 326 } 327 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 328 } 329 330 @Override 331 public void requestChildFocus(View child, View focused) { 332 super.requestChildFocus(child, focused); 333 dispatchChildFocus(mChildrenFocused && focused != null); 334 if (focused != null) { 335 focused.setFocusableInTouchMode(false); 336 } 337 } 338 339 @Override 340 public void clearChildFocus(View child) { 341 super.clearChildFocus(child); 342 dispatchChildFocus(false); 343 } 344 345 @Override 346 public boolean dispatchUnhandledMove(View focused, int direction) { 347 return mChildrenFocused; 348 } 349 350 private void dispatchChildFocus(boolean childIsFocused) { 351 // The host view's background changes when selected, to indicate the focus is inside. 352 setSelected(childIsFocused); 353 } 354 355 public void switchToErrorView() { 356 // Update the widget with 0 Layout id, to reset the view to error view. 357 updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0)); 358 } 359 360 @Override 361 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 362 try { 363 super.onLayout(changed, left, top, right, bottom); 364 } catch (final RuntimeException e) { 365 post(new Runnable() { 366 @Override 367 public void run() { 368 switchToErrorView(); 369 } 370 }); 371 } 372 373 mIsScrollable = checkScrollableRecursively(this); 374 } 375 376 @Override 377 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 378 super.onInitializeAccessibilityNodeInfo(info); 379 info.setClassName(getClass().getName()); 380 } 381 382 @Override 383 protected void onWindowVisibilityChanged(int visibility) { 384 super.onWindowVisibilityChanged(visibility); 385 maybeRegisterAutoAdvance(); 386 } 387 388 private void checkIfAutoAdvance() { 389 boolean isAutoAdvance = false; 390 Advanceable target = getAdvanceable(); 391 if (target != null) { 392 isAutoAdvance = true; 393 target.fyiWillBeAdvancedByHostKThx(); 394 } 395 396 boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0; 397 if (isAutoAdvance != wasAutoAdvance) { 398 if (isAutoAdvance) { 399 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true); 400 } else { 401 sAutoAdvanceWidgetIds.delete(getAppWidgetId()); 402 } 403 maybeRegisterAutoAdvance(); 404 } 405 } 406 407 private Advanceable getAdvanceable() { 408 AppWidgetProviderInfo info = getAppWidgetInfo(); 409 if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) { 410 return null; 411 } 412 View v = findViewById(info.autoAdvanceViewId); 413 return (v instanceof Advanceable) ? (Advanceable) v : null; 414 } 415 416 private void maybeRegisterAutoAdvance() { 417 Handler handler = getHandler(); 418 boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null 419 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0); 420 if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) { 421 mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance; 422 if (mAutoAdvanceRunnable == null) { 423 mAutoAdvanceRunnable = new Runnable() { 424 @Override 425 public void run() { 426 runAutoAdvance(); 427 } 428 }; 429 } 430 431 handler.removeCallbacks(mAutoAdvanceRunnable); 432 scheduleNextAdvance(); 433 } 434 } 435 436 private void scheduleNextAdvance() { 437 if (!mIsAutoAdvanceRegistered) { 438 return; 439 } 440 long now = SystemClock.uptimeMillis(); 441 long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) + 442 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()); 443 Handler handler = getHandler(); 444 if (handler != null) { 445 handler.postAtTime(mAutoAdvanceRunnable, advanceTime); 446 } 447 } 448 449 private void runAutoAdvance() { 450 Advanceable target = getAdvanceable(); 451 if (target != null) { 452 target.advance(); 453 } 454 scheduleNextAdvance(); 455 } 456 457 public void setScaleToFit(float scale) { 458 mScaleToFit = scale; 459 setScaleX(scale); 460 setScaleY(scale); 461 } 462 463 public float getScaleToFit() { 464 return mScaleToFit; 465 } 466 467 public void setTranslationForCentering(float x, float y) { 468 mTranslationForCentering.set(x, y); 469 setTranslationX(x); 470 setTranslationY(y); 471 } 472 473 public PointF getTranslationForCentering() { 474 return mTranslationForCentering; 475 } 476 } 477