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.Rect; 23 import android.os.Handler; 24 import android.os.SystemClock; 25 import android.util.SparseBooleanArray; 26 import android.util.SparseIntArray; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.ViewDebug; 30 import android.view.ViewGroup; 31 import android.view.accessibility.AccessibilityNodeInfo; 32 import android.widget.AdapterView; 33 import android.widget.Advanceable; 34 import android.widget.RemoteViews; 35 36 import androidx.annotation.Nullable; 37 38 import com.android.launcher3.CheckLongPressHelper; 39 import com.android.launcher3.Launcher; 40 import com.android.launcher3.R; 41 import com.android.launcher3.Utilities; 42 import com.android.launcher3.dragndrop.DragLayer; 43 import com.android.launcher3.model.data.ItemInfo; 44 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 45 import com.android.launcher3.util.Themes; 46 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener; 47 48 /** 49 * {@inheritDoc} 50 */ 51 public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView 52 implements TouchCompleteListener, View.OnLongClickListener, 53 LocalColorExtractor.Listener { 54 55 // Related to the auto-advancing of widgets 56 private static final long ADVANCE_INTERVAL = 20000; 57 private static final long ADVANCE_STAGGER = 250; 58 59 // Maintains a list of widget ids which are supposed to be auto advanced. 60 private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray(); 61 // Maximum duration for which updates can be deferred. 62 private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000; 63 64 private final Rect mTempRect = new Rect(); 65 private final CheckLongPressHelper mLongPressHelper; 66 protected final Launcher mLauncher; 67 68 @ViewDebug.ExportedProperty(category = "launcher") 69 private boolean mReinflateOnConfigChange; 70 71 // Maintain the color manager. 72 private final LocalColorExtractor mColorExtractor; 73 74 private boolean mIsScrollable; 75 private boolean mIsAttachedToWindow; 76 private boolean mIsAutoAdvanceRegistered; 77 private Runnable mAutoAdvanceRunnable; 78 79 private long mDeferUpdatesUntilMillis = 0; 80 private RemoteViews mDeferredRemoteViews; 81 private boolean mHasDeferredColorChange = false; 82 private @Nullable SparseIntArray mDeferredColorChange = null; 83 84 // The following member variables are only used during drag-n-drop. 85 private boolean mIsInDragMode = false; 86 /** The drag content width which is only set when the drag content scale is not 1f. */ 87 private int mDragContentWidth = 0; 88 /** The drag content height which is only set when the drag content scale is not 1f. */ 89 private int mDragContentHeight = 0; 90 LauncherAppWidgetHostView(Context context)91 public LauncherAppWidgetHostView(Context context) { 92 super(context); 93 mLauncher = Launcher.getLauncher(context); 94 mLongPressHelper = new CheckLongPressHelper(this, this); 95 setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); 96 setBackgroundResource(R.drawable.widget_internal_focus_bg); 97 98 if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) { 99 setOnLightBackground(true); 100 } 101 mColorExtractor = LocalColorExtractor.newInstance(getContext()); 102 } 103 104 @Override setColorResources(@ullable SparseIntArray colors)105 public void setColorResources(@Nullable SparseIntArray colors) { 106 if (colors == null) { 107 resetColorResources(); 108 } else { 109 super.setColorResources(colors); 110 } 111 } 112 113 @Override onLongClick(View view)114 public boolean onLongClick(View view) { 115 if (mIsScrollable) { 116 DragLayer dragLayer = mLauncher.getDragLayer(); 117 dragLayer.requestDisallowInterceptTouchEvent(false); 118 } 119 view.performLongClick(); 120 return true; 121 } 122 123 @Override updateAppWidget(RemoteViews remoteViews)124 public void updateAppWidget(RemoteViews remoteViews) { 125 if (isDeferringUpdates()) { 126 mDeferredRemoteViews = remoteViews; 127 return; 128 } 129 mDeferredRemoteViews = null; 130 131 super.updateAppWidget(remoteViews); 132 133 // The provider info or the views might have changed. 134 checkIfAutoAdvance(); 135 136 // It is possible that widgets can receive updates while launcher is not in the foreground. 137 // Consequently, the widgets will be inflated for the orientation of the foreground activity 138 // (framework issue). On resuming, we ensure that any widgets are inflated for the current 139 // orientation. 140 mReinflateOnConfigChange = !isSameOrientation(); 141 } 142 isSameOrientation()143 private boolean isSameOrientation() { 144 return mLauncher.getResources().getConfiguration().orientation == 145 mLauncher.getOrientation(); 146 } 147 checkScrollableRecursively(ViewGroup viewGroup)148 private boolean checkScrollableRecursively(ViewGroup viewGroup) { 149 if (viewGroup instanceof AdapterView) { 150 return true; 151 } else { 152 for (int i = 0; i < viewGroup.getChildCount(); i++) { 153 View child = viewGroup.getChildAt(i); 154 if (child instanceof ViewGroup) { 155 if (checkScrollableRecursively((ViewGroup) child)) { 156 return true; 157 } 158 } 159 } 160 } 161 return false; 162 } 163 164 /** 165 * Returns true if the application of {@link RemoteViews} through {@link #updateAppWidget} and 166 * colors through {@link #onColorsChanged} are currently being deferred. 167 * @see #beginDeferringUpdates() 168 */ isDeferringUpdates()169 private boolean isDeferringUpdates() { 170 return SystemClock.uptimeMillis() < mDeferUpdatesUntilMillis; 171 } 172 173 /** 174 * Begin deferring the application of any {@link RemoteViews} updates made through 175 * {@link #updateAppWidget} and color changes through {@link #onColorsChanged} until 176 * {@link #endDeferringUpdates()} has been called or the next {@link #updateAppWidget} or 177 * {@link #onColorsChanged} call after {@link #UPDATE_LOCK_TIMEOUT_MILLIS} have elapsed. 178 */ beginDeferringUpdates()179 public void beginDeferringUpdates() { 180 mDeferUpdatesUntilMillis = SystemClock.uptimeMillis() + UPDATE_LOCK_TIMEOUT_MILLIS; 181 } 182 183 /** 184 * Stop deferring the application of {@link RemoteViews} updates made through 185 * {@link #updateAppWidget} and color changes made through {@link #onColorsChanged} and apply 186 * any deferred updates. 187 */ endDeferringUpdates()188 public void endDeferringUpdates() { 189 RemoteViews remoteViews; 190 SparseIntArray deferredColors; 191 boolean hasDeferredColors; 192 mDeferUpdatesUntilMillis = 0; 193 remoteViews = mDeferredRemoteViews; 194 mDeferredRemoteViews = null; 195 deferredColors = mDeferredColorChange; 196 hasDeferredColors = mHasDeferredColorChange; 197 mDeferredColorChange = null; 198 mHasDeferredColorChange = false; 199 200 if (remoteViews != null) { 201 updateAppWidget(remoteViews); 202 } 203 if (hasDeferredColors) { 204 onColorsChanged(deferredColors); 205 } 206 } 207 onInterceptTouchEvent(MotionEvent ev)208 public boolean onInterceptTouchEvent(MotionEvent ev) { 209 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 210 DragLayer dragLayer = mLauncher.getDragLayer(); 211 if (mIsScrollable) { 212 dragLayer.requestDisallowInterceptTouchEvent(true); 213 } 214 dragLayer.setTouchCompleteListener(this); 215 } 216 mLongPressHelper.onTouchEvent(ev); 217 return mLongPressHelper.hasPerformedLongPress(); 218 } 219 onTouchEvent(MotionEvent ev)220 public boolean onTouchEvent(MotionEvent ev) { 221 mLongPressHelper.onTouchEvent(ev); 222 // We want to keep receiving though events to be able to cancel long press on ACTION_UP 223 return true; 224 } 225 226 @Override onAttachedToWindow()227 protected void onAttachedToWindow() { 228 super.onAttachedToWindow(); 229 mIsAttachedToWindow = true; 230 checkIfAutoAdvance(); 231 mColorExtractor.setListener(this); 232 } 233 234 @Override onDetachedFromWindow()235 protected void onDetachedFromWindow() { 236 super.onDetachedFromWindow(); 237 238 // We can't directly use isAttachedToWindow() here, as this is called before the internal 239 // state is updated. So isAttachedToWindow() will return true until next frame. 240 mIsAttachedToWindow = false; 241 checkIfAutoAdvance(); 242 mColorExtractor.setListener(null); 243 } 244 245 @Override cancelLongPress()246 public void cancelLongPress() { 247 super.cancelLongPress(); 248 mLongPressHelper.cancelLongPress(); 249 } 250 251 @Override getAppWidgetInfo()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 onTouchComplete()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 onLayout(boolean changed, int left, int top, int right, int bottom)271 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 272 super.onLayout(changed, left, top, right, bottom); 273 mIsScrollable = checkScrollableRecursively(this); 274 275 if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo) { 276 LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); 277 mTempRect.set(left, top, right, bottom); 278 mColorExtractor.setWorkspaceLocation(mTempRect, (View) getParent(), info.screenId); 279 } 280 } 281 282 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)283 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 284 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 285 if (mIsInDragMode && mDragContentWidth > 0 && mDragContentHeight > 0 286 && getChildCount() == 1) { 287 measureChild(getChildAt(0), MeasureSpec.getSize(mDragContentWidth), 288 MeasureSpec.getSize(mDragContentHeight)); 289 } 290 } 291 292 /** Starts the drag mode. */ startDrag()293 public void startDrag() { 294 mIsInDragMode = true; 295 // In the case of dragging a scaled preview from widgets picker, we should reuse the 296 // previously measured dimension from WidgetCell#measureAndComputeWidgetPreviewScale, which 297 // measures the dimension of a widget preview without its parent's bound before scaling 298 // down. 299 if ((getScaleX() != 1f || getScaleY() != 1f) && getChildCount() == 1) { 300 mDragContentWidth = getChildAt(0).getMeasuredWidth(); 301 mDragContentHeight = getChildAt(0).getMeasuredHeight(); 302 } 303 } 304 305 /** Handles a drag event occurred on a workspace page corresponding to the {@code screenId}. */ handleDrag(Rect rectInView, View view, int screenId)306 public void handleDrag(Rect rectInView, View view, int screenId) { 307 if (mIsInDragMode) { 308 mColorExtractor.setWorkspaceLocation(rectInView, view, screenId); 309 } 310 } 311 312 /** Ends the drag mode. */ endDrag()313 public void endDrag() { 314 mIsInDragMode = false; 315 mDragContentWidth = 0; 316 mDragContentHeight = 0; 317 requestLayout(); 318 } 319 320 @Override onColorsChanged(SparseIntArray colors)321 public void onColorsChanged(SparseIntArray colors) { 322 if (isDeferringUpdates()) { 323 mDeferredColorChange = colors; 324 mHasDeferredColorChange = true; 325 return; 326 } 327 mDeferredColorChange = null; 328 mHasDeferredColorChange = false; 329 330 // setColorResources will reapply the view, which must happen in the UI thread. 331 post(() -> setColorResources(colors)); 332 } 333 334 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)335 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 336 super.onInitializeAccessibilityNodeInfo(info); 337 info.setClassName(getClass().getName()); 338 } 339 340 @Override onWindowVisibilityChanged(int visibility)341 protected void onWindowVisibilityChanged(int visibility) { 342 super.onWindowVisibilityChanged(visibility); 343 maybeRegisterAutoAdvance(); 344 } 345 checkIfAutoAdvance()346 private void checkIfAutoAdvance() { 347 boolean isAutoAdvance = false; 348 Advanceable target = getAdvanceable(); 349 if (target != null) { 350 isAutoAdvance = true; 351 target.fyiWillBeAdvancedByHostKThx(); 352 } 353 354 boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0; 355 if (isAutoAdvance != wasAutoAdvance) { 356 if (isAutoAdvance) { 357 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true); 358 } else { 359 sAutoAdvanceWidgetIds.delete(getAppWidgetId()); 360 } 361 maybeRegisterAutoAdvance(); 362 } 363 } 364 getAdvanceable()365 private Advanceable getAdvanceable() { 366 AppWidgetProviderInfo info = getAppWidgetInfo(); 367 if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) { 368 return null; 369 } 370 View v = findViewById(info.autoAdvanceViewId); 371 return (v instanceof Advanceable) ? (Advanceable) v : null; 372 } 373 maybeRegisterAutoAdvance()374 private void maybeRegisterAutoAdvance() { 375 Handler handler = getHandler(); 376 boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null 377 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0); 378 if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) { 379 mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance; 380 if (mAutoAdvanceRunnable == null) { 381 mAutoAdvanceRunnable = this::runAutoAdvance; 382 } 383 384 handler.removeCallbacks(mAutoAdvanceRunnable); 385 scheduleNextAdvance(); 386 } 387 } 388 scheduleNextAdvance()389 private void scheduleNextAdvance() { 390 if (!mIsAutoAdvanceRegistered) { 391 return; 392 } 393 long now = SystemClock.uptimeMillis(); 394 long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) + 395 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()); 396 Handler handler = getHandler(); 397 if (handler != null) { 398 handler.postAtTime(mAutoAdvanceRunnable, advanceTime); 399 } 400 } 401 runAutoAdvance()402 private void runAutoAdvance() { 403 Advanceable target = getAdvanceable(); 404 if (target != null) { 405 target.advance(); 406 } 407 scheduleNextAdvance(); 408 } 409 410 @Override onConfigurationChanged(Configuration newConfig)411 protected void onConfigurationChanged(Configuration newConfig) { 412 super.onConfigurationChanged(newConfig); 413 414 // Only reinflate when the final configuration is same as the required configuration 415 if (mReinflateOnConfigChange && isSameOrientation()) { 416 mReinflateOnConfigChange = false; 417 reInflate(); 418 } 419 } 420 reInflate()421 public void reInflate() { 422 if (!isAttachedToWindow()) { 423 return; 424 } 425 LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); 426 if (info == null) { 427 // This occurs when LauncherAppWidgetHostView is used to render a preview layout. 428 return; 429 } 430 // Remove and rebind the current widget (which was inflated in the wrong 431 // orientation), but don't delete it from the database 432 mLauncher.removeItem(this, info, false /* deleteFromDb */); 433 mLauncher.bindAppWidget(info); 434 } 435 436 @Override shouldAllowDirectClick()437 protected boolean shouldAllowDirectClick() { 438 if (getTag() instanceof ItemInfo) { 439 ItemInfo item = (ItemInfo) getTag(); 440 return item.spanX == 1 && item.spanY == 1; 441 } 442 return false; 443 } 444 } 445