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.graphics.Rect; 22 import android.os.Handler; 23 import android.os.Parcelable; 24 import android.os.SystemClock; 25 import android.os.Trace; 26 import android.util.Log; 27 import android.util.SparseArray; 28 import android.util.SparseBooleanArray; 29 import android.util.SparseIntArray; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.accessibility.AccessibilityNodeInfo; 34 import android.widget.AdapterView; 35 import android.widget.Advanceable; 36 import android.widget.RemoteViews; 37 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 41 import com.android.launcher3.CheckLongPressHelper; 42 import com.android.launcher3.Flags; 43 import com.android.launcher3.R; 44 import com.android.launcher3.model.data.ItemInfo; 45 import com.android.launcher3.util.Themes; 46 import com.android.launcher3.views.ActivityContext; 47 import com.android.launcher3.views.BaseDragLayer; 48 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener; 49 50 /** 51 * {@inheritDoc} 52 */ 53 public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView 54 implements TouchCompleteListener, View.OnLongClickListener { 55 56 private static final String TAG = "LauncherAppWidgetHostView"; 57 58 // Related to the auto-advancing of widgets 59 private static final long ADVANCE_INTERVAL = 20000; 60 private static final long ADVANCE_STAGGER = 250; 61 62 private @Nullable CellChildViewPreLayoutListener mCellChildViewPreLayoutListener; 63 64 // Maintains a list of widget ids which are supposed to be auto advanced. 65 private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray(); 66 // Maximum duration for which updates can be deferred. 67 private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000; 68 69 private static final String TRACE_METHOD_NAME = "appwidget load-widget "; 70 71 private final CheckLongPressHelper mLongPressHelper; 72 protected final ActivityContext mActivityContext; 73 74 private boolean mIsScrollable; 75 private boolean mIsAttachedToWindow; 76 private boolean mIsAutoAdvanceRegistered; 77 private Runnable mAutoAdvanceRunnable; 78 79 private long mDeferUpdatesUntilMillis = 0; 80 RemoteViews mLastRemoteViews; 81 82 private boolean mTrackingWidgetUpdate = false; 83 84 private int mFocusRectOutsets = 0; 85 LauncherAppWidgetHostView(Context context)86 public LauncherAppWidgetHostView(Context context) { 87 super(context); 88 mActivityContext = ActivityContext.lookupContext(context); 89 mLongPressHelper = new CheckLongPressHelper(this, this); 90 setAccessibilityDelegate(mActivityContext.getAccessibilityDelegate()); 91 setBackgroundResource(R.drawable.widget_internal_focus_bg); 92 if (Flags.enableFocusOutline()) { 93 setDefaultFocusHighlightEnabled(false); 94 mFocusRectOutsets = context.getResources().getDimensionPixelSize( 95 R.dimen.focus_rect_widget_outsets); 96 } 97 98 if (Themes.getAttrBoolean(context, R.attr.isWorkspaceDarkText)) { 99 setOnLightBackground(true); 100 } 101 } 102 103 @Override setColorResources(@ullable SparseIntArray colors)104 public void setColorResources(@Nullable SparseIntArray colors) { 105 if (colors == null) { 106 resetColorResources(); 107 } else { 108 super.setColorResources(colors); 109 } 110 } 111 112 @Override onLongClick(View view)113 public boolean onLongClick(View view) { 114 if (mIsScrollable) { 115 mActivityContext.getDragLayer().requestDisallowInterceptTouchEvent(false); 116 } 117 view.performLongClick(); 118 return true; 119 } 120 121 @Override setAppWidget(int appWidgetId, AppWidgetProviderInfo info)122 public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) { 123 super.setAppWidget(appWidgetId, info); 124 if (!mTrackingWidgetUpdate) { 125 mTrackingWidgetUpdate = true; 126 Trace.beginAsyncSection(TRACE_METHOD_NAME + info.provider, appWidgetId); 127 Log.i(TAG, "App widget created with id: " + appWidgetId); 128 } 129 } 130 131 @Override updateAppWidget(RemoteViews remoteViews)132 public void updateAppWidget(RemoteViews remoteViews) { 133 if (mTrackingWidgetUpdate && remoteViews != null) { 134 Log.i(TAG, "App widget with id: " + getAppWidgetId() + " loaded"); 135 Trace.endAsyncSection( 136 TRACE_METHOD_NAME + getAppWidgetInfo().provider, getAppWidgetId()); 137 mTrackingWidgetUpdate = false; 138 } 139 if (isDeferringUpdates()) { 140 mLastRemoteViews = remoteViews; 141 return; 142 } 143 mLastRemoteViews = null; 144 145 super.updateAppWidget(remoteViews); 146 147 // The provider info or the views might have changed. 148 checkIfAutoAdvance(); 149 } 150 checkScrollableRecursively(ViewGroup viewGroup)151 private boolean checkScrollableRecursively(ViewGroup viewGroup) { 152 if (viewGroup instanceof AdapterView) { 153 return true; 154 } else { 155 for (int i = 0; i < viewGroup.getChildCount(); i++) { 156 View child = viewGroup.getChildAt(i); 157 if (child instanceof ViewGroup) { 158 if (checkScrollableRecursively((ViewGroup) child)) { 159 return true; 160 } 161 } 162 } 163 } 164 return false; 165 } 166 167 /** 168 * Returns true if the application of {@link RemoteViews} through {@link #updateAppWidget} are 169 * currently being deferred. 170 * @see #beginDeferringUpdates() 171 */ isDeferringUpdates()172 private boolean isDeferringUpdates() { 173 return SystemClock.uptimeMillis() < mDeferUpdatesUntilMillis; 174 } 175 176 /** 177 * Begin deferring the application of any {@link RemoteViews} updates made through 178 * {@link #updateAppWidget} until {@link #endDeferringUpdates()} has been called or the next 179 * {@link #updateAppWidget} call after {@link #UPDATE_LOCK_TIMEOUT_MILLIS} have elapsed. 180 */ beginDeferringUpdates()181 public void beginDeferringUpdates() { 182 mDeferUpdatesUntilMillis = SystemClock.uptimeMillis() + UPDATE_LOCK_TIMEOUT_MILLIS; 183 } 184 185 /** 186 * Stop deferring the application of {@link RemoteViews} updates made through 187 * {@link #updateAppWidget} and apply any deferred updates. 188 */ endDeferringUpdates()189 public void endDeferringUpdates() { 190 RemoteViews remoteViews; 191 mDeferUpdatesUntilMillis = 0; 192 remoteViews = mLastRemoteViews; 193 194 if (remoteViews != null) { 195 updateAppWidget(remoteViews); 196 } 197 } 198 onInterceptTouchEvent(MotionEvent ev)199 public boolean onInterceptTouchEvent(MotionEvent ev) { 200 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 201 BaseDragLayer dragLayer = mActivityContext.getDragLayer(); 202 if (mIsScrollable) { 203 dragLayer.requestDisallowInterceptTouchEvent(true); 204 } 205 dragLayer.setTouchCompleteListener(this); 206 } 207 mLongPressHelper.onTouchEvent(ev); 208 return mLongPressHelper.hasPerformedLongPress(); 209 } 210 onTouchEvent(MotionEvent ev)211 public boolean onTouchEvent(MotionEvent ev) { 212 mLongPressHelper.onTouchEvent(ev); 213 // We want to keep receiving though events to be able to cancel long press on ACTION_UP 214 return true; 215 } 216 217 @Override onAttachedToWindow()218 protected void onAttachedToWindow() { 219 super.onAttachedToWindow(); 220 mIsAttachedToWindow = true; 221 checkIfAutoAdvance(); 222 } 223 224 @Override onDetachedFromWindow()225 protected void onDetachedFromWindow() { 226 super.onDetachedFromWindow(); 227 228 // We can't directly use isAttachedToWindow() here, as this is called before the internal 229 // state is updated. So isAttachedToWindow() will return true until next frame. 230 mIsAttachedToWindow = false; 231 checkIfAutoAdvance(); 232 } 233 234 @Override cancelLongPress()235 public void cancelLongPress() { 236 super.cancelLongPress(); 237 mLongPressHelper.cancelLongPress(); 238 } 239 240 @Override getAppWidgetInfo()241 public AppWidgetProviderInfo getAppWidgetInfo() { 242 AppWidgetProviderInfo info = super.getAppWidgetInfo(); 243 if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) { 244 throw new IllegalStateException("Launcher widget must have" 245 + " LauncherAppWidgetProviderInfo"); 246 } 247 return info; 248 } 249 250 @Override getFocusedRect(Rect r)251 public void getFocusedRect(Rect r) { 252 super.getFocusedRect(r); 253 // Outset to a larger rect for drawing a padding between focus outline and widget 254 r.inset(mFocusRectOutsets, mFocusRectOutsets); 255 } 256 257 @Override onTouchComplete()258 public void onTouchComplete() { 259 if (!mLongPressHelper.hasPerformedLongPress()) { 260 // If a long press has been performed, we don't want to clear the record of that since 261 // we still may be receiving a touch up which we want to intercept 262 mLongPressHelper.cancelLongPress(); 263 } 264 } 265 266 @Override onLayout(boolean changed, int left, int top, int right, int bottom)267 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 268 super.onLayout(changed, left, top, right, bottom); 269 mIsScrollable = checkScrollableRecursively(this); 270 } 271 272 /** 273 * Set the pre-layout listener 274 * @param listener The listener to be notified when {@code CellLayout} is to layout this view 275 */ setCellChildViewPreLayoutListener( @onNull CellChildViewPreLayoutListener listener)276 public void setCellChildViewPreLayoutListener( 277 @NonNull CellChildViewPreLayoutListener listener) { 278 mCellChildViewPreLayoutListener = listener; 279 } 280 281 /** @return The current cell layout listener */ 282 @Nullable getCellChildViewPreLayoutListener()283 public CellChildViewPreLayoutListener getCellChildViewPreLayoutListener() { 284 return mCellChildViewPreLayoutListener; 285 } 286 287 /** Clear the listener for the pre-layout in CellLayout */ clearCellChildViewPreLayoutListener()288 public void clearCellChildViewPreLayoutListener() { 289 mCellChildViewPreLayoutListener = null; 290 } 291 292 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)293 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 294 super.onInitializeAccessibilityNodeInfo(info); 295 info.setClassName(getClass().getName()); 296 } 297 298 @Override onWindowVisibilityChanged(int visibility)299 protected void onWindowVisibilityChanged(int visibility) { 300 super.onWindowVisibilityChanged(visibility); 301 maybeRegisterAutoAdvance(); 302 } 303 checkIfAutoAdvance()304 private void checkIfAutoAdvance() { 305 boolean isAutoAdvance = false; 306 Advanceable target = getAdvanceable(); 307 if (target != null) { 308 isAutoAdvance = true; 309 target.fyiWillBeAdvancedByHostKThx(); 310 } 311 312 boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0; 313 if (isAutoAdvance != wasAutoAdvance) { 314 if (isAutoAdvance) { 315 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true); 316 } else { 317 sAutoAdvanceWidgetIds.delete(getAppWidgetId()); 318 } 319 maybeRegisterAutoAdvance(); 320 } 321 } 322 getAdvanceable()323 private Advanceable getAdvanceable() { 324 AppWidgetProviderInfo info = getAppWidgetInfo(); 325 if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) { 326 return null; 327 } 328 View v = findViewById(info.autoAdvanceViewId); 329 return (v instanceof Advanceable) ? (Advanceable) v : null; 330 } 331 maybeRegisterAutoAdvance()332 private void maybeRegisterAutoAdvance() { 333 Handler handler = getHandler(); 334 boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null 335 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0); 336 if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) { 337 mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance; 338 if (mAutoAdvanceRunnable == null) { 339 mAutoAdvanceRunnable = this::runAutoAdvance; 340 } 341 342 handler.removeCallbacks(mAutoAdvanceRunnable); 343 scheduleNextAdvance(); 344 } 345 } 346 scheduleNextAdvance()347 private void scheduleNextAdvance() { 348 if (!mIsAutoAdvanceRegistered) { 349 return; 350 } 351 long now = SystemClock.uptimeMillis(); 352 long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) + 353 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()); 354 Handler handler = getHandler(); 355 if (handler != null) { 356 handler.postAtTime(mAutoAdvanceRunnable, advanceTime); 357 } 358 } 359 runAutoAdvance()360 private void runAutoAdvance() { 361 Advanceable target = getAdvanceable(); 362 if (target != null) { 363 target.advance(); 364 } 365 scheduleNextAdvance(); 366 } 367 368 @Override shouldAllowDirectClick()369 protected boolean shouldAllowDirectClick() { 370 if (getTag() instanceof ItemInfo item) { 371 return item.spanX == 1 && item.spanY == 1; 372 } 373 return false; 374 } 375 376 /** 377 * Listener interface to be called when {@code CellLayout} is about to layout this child view 378 */ 379 public interface CellChildViewPreLayoutListener { 380 /** 381 * Notify the bound changes to this view on pre-layout 382 * @param v The view which the listener is set for 383 * @param left The new left coordinate of this view 384 * @param top The new top coordinate of this view 385 * @param right The new right coordinate of this view 386 * @param bottom The new bottom coordinate of this view 387 */ notifyBoundChangeOnPreLayout(View v, int left, int top, int right, int bottom)388 void notifyBoundChangeOnPreLayout(View v, int left, int top, int right, int bottom); 389 } 390 391 @Override dispatchRestoreInstanceState(SparseArray<Parcelable> container)392 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 393 try { 394 super.dispatchRestoreInstanceState(container); 395 } catch (Exception e) { 396 Log.i(TAG, "Exception: " + e); 397 } 398 } 399 } 400