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