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.AppWidgetProviderInfo; 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.graphics.Canvas; 23 import android.graphics.Outline; 24 import android.graphics.Rect; 25 import android.graphics.RectF; 26 import android.os.Handler; 27 import android.os.SystemClock; 28 import android.util.SparseBooleanArray; 29 import android.util.SparseIntArray; 30 import android.view.LayoutInflater; 31 import android.view.MotionEvent; 32 import android.view.View; 33 import android.view.ViewDebug; 34 import android.view.ViewGroup; 35 import android.view.ViewOutlineProvider; 36 import android.view.accessibility.AccessibilityNodeInfo; 37 import android.widget.AdapterView; 38 import android.widget.Advanceable; 39 import android.widget.RemoteViews; 40 41 import androidx.annotation.NonNull; 42 import androidx.annotation.Nullable; 43 import androidx.annotation.UiThread; 44 45 import com.android.launcher3.CheckLongPressHelper; 46 import com.android.launcher3.Launcher; 47 import com.android.launcher3.R; 48 import com.android.launcher3.Utilities; 49 import com.android.launcher3.Workspace; 50 import com.android.launcher3.dragndrop.DragLayer; 51 import com.android.launcher3.keyboard.ViewGroupFocusHelper; 52 import com.android.launcher3.model.data.ItemInfo; 53 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 54 import com.android.launcher3.util.Executors; 55 import com.android.launcher3.util.Themes; 56 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener; 57 import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener; 58 59 import java.util.List; 60 61 /** 62 * {@inheritDoc} 63 */ 64 public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView 65 implements TouchCompleteListener, View.OnLongClickListener, 66 LocalColorExtractor.Listener { 67 68 private static final String LOG_TAG = "LauncherAppWidgetHostView"; 69 70 // Related to the auto-advancing of widgets 71 private static final long ADVANCE_INTERVAL = 20000; 72 private static final long ADVANCE_STAGGER = 250; 73 74 // Maintains a list of widget ids which are supposed to be auto advanced. 75 private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray(); 76 // Maximum duration for which updates can be deferred. 77 private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000; 78 79 protected final LayoutInflater mInflater; 80 81 private final CheckLongPressHelper mLongPressHelper; 82 protected final Launcher mLauncher; 83 private final Workspace mWorkspace; 84 85 @ViewDebug.ExportedProperty(category = "launcher") 86 private boolean mReinflateOnConfigChange; 87 88 // Maintain the color manager. 89 private final LocalColorExtractor mColorExtractor; 90 91 private boolean mIsScrollable; 92 private boolean mIsAttachedToWindow; 93 private boolean mIsAutoAdvanceRegistered; 94 private boolean mIsInDragMode = false; 95 private Runnable mAutoAdvanceRunnable; 96 private RectF mLastLocationRegistered = null; 97 @Nullable private AppWidgetHostViewDragListener mDragListener; 98 99 // Used to store the widget sizes in drag layer coordinates. 100 private final Rect mCurrentWidgetSize = new Rect(); 101 private final Rect mWidgetSizeAtDrag = new Rect(); 102 103 private final RectF mTempRectF = new RectF(); 104 private final Rect mEnforcedRectangle = new Rect(); 105 private final float mEnforcedCornerRadius; 106 private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() { 107 @Override 108 public void getOutline(View view, Outline outline) { 109 if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) { 110 outline.setEmpty(); 111 } else { 112 outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius); 113 } 114 } 115 }; 116 private final Object mUpdateLock = new Object(); 117 private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper; 118 private long mDeferUpdatesUntilMillis = 0; 119 private RemoteViews mDeferredRemoteViews; 120 private boolean mHasDeferredColorChange = false; 121 private @Nullable SparseIntArray mDeferredColorChange = null; 122 private boolean mEnableColorExtraction = true; 123 LauncherAppWidgetHostView(Context context)124 public LauncherAppWidgetHostView(Context context) { 125 super(context); 126 mLauncher = Launcher.getLauncher(context); 127 mWorkspace = mLauncher.getWorkspace(); 128 mLongPressHelper = new CheckLongPressHelper(this, this); 129 mInflater = LayoutInflater.from(context); 130 setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); 131 setBackgroundResource(R.drawable.widget_internal_focus_bg); 132 133 setExecutor(Executors.THREAD_POOL_EXECUTOR); 134 if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) { 135 setOnLightBackground(true); 136 } 137 mColorExtractor = LocalColorExtractor.newInstance(getContext()); 138 mColorExtractor.setListener(this); 139 140 mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext()); 141 mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer()); 142 } 143 144 @Override setColorResources(@ullable SparseIntArray colors)145 public void setColorResources(@Nullable SparseIntArray colors) { 146 if (colors == null) { 147 resetColorResources(); 148 } else { 149 super.setColorResources(colors); 150 } 151 } 152 153 @Override onDraw(Canvas canvas)154 protected void onDraw(Canvas canvas) { 155 super.onDraw(canvas); 156 if (mIsInDragMode && mDragListener != null) { 157 mDragListener.onDragContentChanged(); 158 } 159 } 160 161 @Override onLongClick(View view)162 public boolean onLongClick(View view) { 163 if (mIsScrollable) { 164 DragLayer dragLayer = mLauncher.getDragLayer(); 165 dragLayer.requestDisallowInterceptTouchEvent(false); 166 } 167 view.performLongClick(); 168 return true; 169 } 170 171 @Override getErrorView()172 protected View getErrorView() { 173 return mInflater.inflate(R.layout.appwidget_error, this, false); 174 } 175 176 @Override updateAppWidget(RemoteViews remoteViews)177 public void updateAppWidget(RemoteViews remoteViews) { 178 synchronized (mUpdateLock) { 179 if (isDeferringUpdates()) { 180 mDeferredRemoteViews = remoteViews; 181 return; 182 } 183 mDeferredRemoteViews = null; 184 } 185 186 super.updateAppWidget(remoteViews); 187 188 // The provider info or the views might have changed. 189 checkIfAutoAdvance(); 190 191 // It is possible that widgets can receive updates while launcher is not in the foreground. 192 // Consequently, the widgets will be inflated for the orientation of the foreground activity 193 // (framework issue). On resuming, we ensure that any widgets are inflated for the current 194 // orientation. 195 mReinflateOnConfigChange = !isSameOrientation(); 196 } 197 isSameOrientation()198 private boolean isSameOrientation() { 199 return mLauncher.getResources().getConfiguration().orientation == 200 mLauncher.getOrientation(); 201 } 202 checkScrollableRecursively(ViewGroup viewGroup)203 private boolean checkScrollableRecursively(ViewGroup viewGroup) { 204 if (viewGroup instanceof AdapterView) { 205 return true; 206 } else { 207 for (int i = 0; i < viewGroup.getChildCount(); i++) { 208 View child = viewGroup.getChildAt(i); 209 if (child instanceof ViewGroup) { 210 if (checkScrollableRecursively((ViewGroup) child)) { 211 return true; 212 } 213 } 214 } 215 } 216 return false; 217 } 218 219 /** 220 * Returns true if the application of {@link RemoteViews} through {@link #updateAppWidget} and 221 * colors through {@link #onColorsChanged} are currently being deferred. 222 * @see #beginDeferringUpdates() 223 */ isDeferringUpdates()224 private boolean isDeferringUpdates() { 225 return SystemClock.uptimeMillis() < mDeferUpdatesUntilMillis; 226 } 227 228 /** 229 * Begin deferring the application of any {@link RemoteViews} updates made through 230 * {@link #updateAppWidget} and color changes through {@link #onColorsChanged} until 231 * {@link #endDeferringUpdates()} has been called or the next {@link #updateAppWidget} or 232 * {@link #onColorsChanged} call after {@link #UPDATE_LOCK_TIMEOUT_MILLIS} have elapsed. 233 */ beginDeferringUpdates()234 public void beginDeferringUpdates() { 235 synchronized (mUpdateLock) { 236 mDeferUpdatesUntilMillis = SystemClock.uptimeMillis() + UPDATE_LOCK_TIMEOUT_MILLIS; 237 } 238 } 239 240 /** 241 * Stop deferring the application of {@link RemoteViews} updates made through 242 * {@link #updateAppWidget} and color changes made through {@link #onColorsChanged} and apply 243 * any deferred updates. 244 */ endDeferringUpdates()245 public void endDeferringUpdates() { 246 RemoteViews remoteViews; 247 SparseIntArray deferredColors; 248 boolean hasDeferredColors; 249 synchronized (mUpdateLock) { 250 mDeferUpdatesUntilMillis = 0; 251 remoteViews = mDeferredRemoteViews; 252 mDeferredRemoteViews = null; 253 deferredColors = mDeferredColorChange; 254 hasDeferredColors = mHasDeferredColorChange; 255 mDeferredColorChange = null; 256 mHasDeferredColorChange = false; 257 } 258 if (remoteViews != null) { 259 updateAppWidget(remoteViews); 260 } 261 if (hasDeferredColors) { 262 onColorsChanged(null /* rectF */, deferredColors); 263 } 264 } 265 onInterceptTouchEvent(MotionEvent ev)266 public boolean onInterceptTouchEvent(MotionEvent ev) { 267 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 268 DragLayer dragLayer = mLauncher.getDragLayer(); 269 if (mIsScrollable) { 270 dragLayer.requestDisallowInterceptTouchEvent(true); 271 } 272 dragLayer.setTouchCompleteListener(this); 273 } 274 mLongPressHelper.onTouchEvent(ev); 275 return mLongPressHelper.hasPerformedLongPress(); 276 } 277 onTouchEvent(MotionEvent ev)278 public boolean onTouchEvent(MotionEvent ev) { 279 mLongPressHelper.onTouchEvent(ev); 280 // We want to keep receiving though events to be able to cancel long press on ACTION_UP 281 return true; 282 } 283 284 @Override onAttachedToWindow()285 protected void onAttachedToWindow() { 286 super.onAttachedToWindow(); 287 288 mIsAttachedToWindow = true; 289 checkIfAutoAdvance(); 290 291 if (mLastLocationRegistered != null) { 292 mColorExtractor.addLocation(List.of(mLastLocationRegistered)); 293 } 294 } 295 296 @Override onDetachedFromWindow()297 protected void onDetachedFromWindow() { 298 super.onDetachedFromWindow(); 299 300 // We can't directly use isAttachedToWindow() here, as this is called before the internal 301 // state is updated. So isAttachedToWindow() will return true until next frame. 302 mIsAttachedToWindow = false; 303 checkIfAutoAdvance(); 304 mColorExtractor.removeLocations(); 305 } 306 307 @Override cancelLongPress()308 public void cancelLongPress() { 309 super.cancelLongPress(); 310 mLongPressHelper.cancelLongPress(); 311 } 312 313 @Override getAppWidgetInfo()314 public AppWidgetProviderInfo getAppWidgetInfo() { 315 AppWidgetProviderInfo info = super.getAppWidgetInfo(); 316 if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) { 317 throw new IllegalStateException("Launcher widget must have" 318 + " LauncherAppWidgetProviderInfo"); 319 } 320 return info; 321 } 322 323 @Override onTouchComplete()324 public void onTouchComplete() { 325 if (!mLongPressHelper.hasPerformedLongPress()) { 326 // If a long press has been performed, we don't want to clear the record of that since 327 // we still may be receiving a touch up which we want to intercept 328 mLongPressHelper.cancelLongPress(); 329 } 330 } 331 switchToErrorView()332 public void switchToErrorView() { 333 // Update the widget with 0 Layout id, to reset the view to error view. 334 updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0)); 335 } 336 337 @Override onLayout(boolean changed, int left, int top, int right, int bottom)338 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 339 try { 340 super.onLayout(changed, left, top, right, bottom); 341 } catch (final RuntimeException e) { 342 post(new Runnable() { 343 @Override 344 public void run() { 345 switchToErrorView(); 346 } 347 }); 348 } 349 350 mIsScrollable = checkScrollableRecursively(this); 351 updateColorExtraction(); 352 353 enforceRoundedCorners(); 354 } 355 356 /** Starts the drag mode. */ startDrag(AppWidgetHostViewDragListener dragListener)357 public void startDrag(AppWidgetHostViewDragListener dragListener) { 358 mIsInDragMode = true; 359 mDragListener = dragListener; 360 } 361 362 /** Handles a drag event occurred on a workspace page, {@code pageId}. */ handleDrag(Rect rectInDragLayer, int pageId)363 public void handleDrag(Rect rectInDragLayer, int pageId) { 364 mWidgetSizeAtDrag.set(rectInDragLayer); 365 updateColorExtraction(mWidgetSizeAtDrag, pageId); 366 } 367 368 /** Ends the drag mode. */ endDrag()369 public void endDrag() { 370 mIsInDragMode = false; 371 mDragListener = null; 372 mWidgetSizeAtDrag.setEmpty(); 373 } 374 375 /** 376 * @param rectInDragLayer Rect of widget in drag layer coordinates. 377 * @param pageId The workspace page the widget is on. 378 */ updateColorExtraction(Rect rectInDragLayer, int pageId)379 private void updateColorExtraction(Rect rectInDragLayer, int pageId) { 380 if (!mEnableColorExtraction) return; 381 mColorExtractor.getExtractedRectForViewRect(mLauncher, pageId, rectInDragLayer, mTempRectF); 382 383 if (mTempRectF.isEmpty()) { 384 return; 385 } 386 if (!isSameLocation(mTempRectF, mLastLocationRegistered, /* epsilon= */ 1e-6f)) { 387 if (mLastLocationRegistered != null) { 388 mColorExtractor.removeLocations(); 389 } 390 mLastLocationRegistered = new RectF(mTempRectF); 391 mColorExtractor.addLocation(List.of(mLastLocationRegistered)); 392 } 393 } 394 395 /** 396 * Update the color extraction, using the current position of the app widget. 397 */ updateColorExtraction()398 private void updateColorExtraction() { 399 if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo) { 400 LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); 401 mDragLayerRelativeCoordinateHelper.viewToRect(this, mCurrentWidgetSize); 402 updateColorExtraction(mCurrentWidgetSize, 403 mWorkspace.getPageIndexForScreenId(info.screenId)); 404 } 405 } 406 407 /** 408 * Enables the local color extraction. 409 * 410 * @param updateColors If true, this will update the color extraction using the current location 411 * of the App Widget. 412 */ enableColorExtraction(boolean updateColors)413 public void enableColorExtraction(boolean updateColors) { 414 mEnableColorExtraction = true; 415 if (updateColors) { 416 updateColorExtraction(); 417 } 418 } 419 420 /** 421 * Disables the local color extraction. 422 */ disableColorExtraction()423 public void disableColorExtraction() { 424 mEnableColorExtraction = false; 425 } 426 427 // Compare two location rectangles. Locations are always in the [0;1] range. isSameLocation(@onNull RectF rect1, @Nullable RectF rect2, float epsilon)428 private static boolean isSameLocation(@NonNull RectF rect1, @Nullable RectF rect2, 429 float epsilon) { 430 if (rect2 == null) return false; 431 return isSameCoordinate(rect1.left, rect2.left, epsilon) 432 && isSameCoordinate(rect1.right, rect2.right, epsilon) 433 && isSameCoordinate(rect1.top, rect2.top, epsilon) 434 && isSameCoordinate(rect1.bottom, rect2.bottom, epsilon); 435 } 436 isSameCoordinate(float c1, float c2, float epsilon)437 private static boolean isSameCoordinate(float c1, float c2, float epsilon) { 438 return Math.abs(c1 - c2) < epsilon; 439 } 440 441 @Override onColorsChanged(RectF rectF, SparseIntArray colors)442 public void onColorsChanged(RectF rectF, SparseIntArray colors) { 443 synchronized (mUpdateLock) { 444 if (isDeferringUpdates()) { 445 mDeferredColorChange = colors; 446 mHasDeferredColorChange = true; 447 return; 448 } 449 mDeferredColorChange = null; 450 mHasDeferredColorChange = false; 451 } 452 453 // setColorResources will reapply the view, which must happen in the UI thread. 454 post(() -> setColorResources(colors)); 455 } 456 457 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)458 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 459 super.onInitializeAccessibilityNodeInfo(info); 460 info.setClassName(getClass().getName()); 461 } 462 463 @Override onWindowVisibilityChanged(int visibility)464 protected void onWindowVisibilityChanged(int visibility) { 465 super.onWindowVisibilityChanged(visibility); 466 maybeRegisterAutoAdvance(); 467 } 468 checkIfAutoAdvance()469 private void checkIfAutoAdvance() { 470 boolean isAutoAdvance = false; 471 Advanceable target = getAdvanceable(); 472 if (target != null) { 473 isAutoAdvance = true; 474 target.fyiWillBeAdvancedByHostKThx(); 475 } 476 477 boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0; 478 if (isAutoAdvance != wasAutoAdvance) { 479 if (isAutoAdvance) { 480 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true); 481 } else { 482 sAutoAdvanceWidgetIds.delete(getAppWidgetId()); 483 } 484 maybeRegisterAutoAdvance(); 485 } 486 } 487 getAdvanceable()488 private Advanceable getAdvanceable() { 489 AppWidgetProviderInfo info = getAppWidgetInfo(); 490 if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) { 491 return null; 492 } 493 View v = findViewById(info.autoAdvanceViewId); 494 return (v instanceof Advanceable) ? (Advanceable) v : null; 495 } 496 maybeRegisterAutoAdvance()497 private void maybeRegisterAutoAdvance() { 498 Handler handler = getHandler(); 499 boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null 500 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0); 501 if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) { 502 mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance; 503 if (mAutoAdvanceRunnable == null) { 504 mAutoAdvanceRunnable = this::runAutoAdvance; 505 } 506 507 handler.removeCallbacks(mAutoAdvanceRunnable); 508 scheduleNextAdvance(); 509 } 510 } 511 scheduleNextAdvance()512 private void scheduleNextAdvance() { 513 if (!mIsAutoAdvanceRegistered) { 514 return; 515 } 516 long now = SystemClock.uptimeMillis(); 517 long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) + 518 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()); 519 Handler handler = getHandler(); 520 if (handler != null) { 521 handler.postAtTime(mAutoAdvanceRunnable, advanceTime); 522 } 523 } 524 runAutoAdvance()525 private void runAutoAdvance() { 526 Advanceable target = getAdvanceable(); 527 if (target != null) { 528 target.advance(); 529 } 530 scheduleNextAdvance(); 531 } 532 533 @Override onConfigurationChanged(Configuration newConfig)534 protected void onConfigurationChanged(Configuration newConfig) { 535 super.onConfigurationChanged(newConfig); 536 537 // Only reinflate when the final configuration is same as the required configuration 538 if (mReinflateOnConfigChange && isSameOrientation()) { 539 mReinflateOnConfigChange = false; 540 reInflate(); 541 } 542 } 543 reInflate()544 public void reInflate() { 545 if (!isAttachedToWindow()) { 546 return; 547 } 548 LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); 549 if (info == null) { 550 // This occurs when LauncherAppWidgetHostView is used to render a preview layout. 551 return; 552 } 553 // Remove and rebind the current widget (which was inflated in the wrong 554 // orientation), but don't delete it from the database 555 mLauncher.removeItem(this, info, false /* deleteFromDb */); 556 mLauncher.bindAppWidget(info); 557 } 558 559 @Override shouldAllowDirectClick()560 protected boolean shouldAllowDirectClick() { 561 if (getTag() instanceof ItemInfo) { 562 ItemInfo item = (ItemInfo) getTag(); 563 return item.spanX == 1 && item.spanY == 1; 564 } 565 return false; 566 } 567 568 @UiThread resetRoundedCorners()569 private void resetRoundedCorners() { 570 setOutlineProvider(ViewOutlineProvider.BACKGROUND); 571 setClipToOutline(false); 572 } 573 574 @UiThread enforceRoundedCorners()575 private void enforceRoundedCorners() { 576 if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) { 577 resetRoundedCorners(); 578 return; 579 } 580 View background = RoundedCornerEnforcement.findBackground(this); 581 if (background == null 582 || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) { 583 resetRoundedCorners(); 584 return; 585 } 586 RoundedCornerEnforcement.computeRoundedRectangle(this, 587 background, 588 mEnforcedRectangle); 589 setOutlineProvider(mCornerRadiusEnforcementOutline); 590 setClipToOutline(true); 591 } 592 593 /** Returns the corner radius currently enforced, in pixels. */ getEnforcedCornerRadius()594 public float getEnforcedCornerRadius() { 595 return mEnforcedCornerRadius; 596 } 597 598 /** Returns true if the corner radius are enforced for this App Widget. */ hasEnforcedCornerRadius()599 public boolean hasEnforcedCornerRadius() { 600 return getClipToOutline(); 601 } 602 603 } 604