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