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