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