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.PointF; 23 import android.os.Handler; 24 import android.os.SystemClock; 25 import android.util.SparseBooleanArray; 26 import android.view.LayoutInflater; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.ViewConfiguration; 30 import android.view.ViewDebug; 31 import android.view.ViewGroup; 32 import android.view.accessibility.AccessibilityNodeInfo; 33 import android.widget.AdapterView; 34 import android.widget.Advanceable; 35 import android.widget.RemoteViews; 36 37 import com.android.launcher3.CheckLongPressHelper; 38 import com.android.launcher3.ItemInfo; 39 import com.android.launcher3.Launcher; 40 import com.android.launcher3.LauncherAppWidgetInfo; 41 import com.android.launcher3.LauncherAppWidgetProviderInfo; 42 import com.android.launcher3.R; 43 import com.android.launcher3.SimpleOnStylusPressListener; 44 import com.android.launcher3.StylusEventHelper; 45 import com.android.launcher3.Utilities; 46 import com.android.launcher3.dragndrop.DragLayer; 47 import com.android.launcher3.util.Executors; 48 import com.android.launcher3.util.Themes; 49 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener; 50 51 /** 52 * {@inheritDoc} 53 */ 54 public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView 55 implements TouchCompleteListener, View.OnLongClickListener { 56 57 // Related to the auto-advancing of widgets 58 private static final long ADVANCE_INTERVAL = 20000; 59 private static final long ADVANCE_STAGGER = 250; 60 61 // Maintains a list of widget ids which are supposed to be auto advanced. 62 private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray(); 63 64 protected final LayoutInflater mInflater; 65 66 private final CheckLongPressHelper mLongPressHelper; 67 private final StylusEventHelper mStylusEventHelper; 68 protected final Launcher mLauncher; 69 70 @ViewDebug.ExportedProperty(category = "launcher") 71 private boolean mReinflateOnConfigChange; 72 73 private float mSlop; 74 75 private boolean mIsScrollable; 76 private boolean mIsAttachedToWindow; 77 private boolean mIsAutoAdvanceRegistered; 78 private Runnable mAutoAdvanceRunnable; 79 80 /** 81 * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY. 82 */ 83 private float mScaleToFit = 1f; 84 85 /** 86 * The translation values to center the widget within its cellspans. 87 */ 88 private final PointF mTranslationForCentering = new PointF(0, 0); 89 LauncherAppWidgetHostView(Context context)90 public LauncherAppWidgetHostView(Context context) { 91 super(context); 92 mLauncher = Launcher.getLauncher(context); 93 mLongPressHelper = new CheckLongPressHelper(this, this); 94 mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this); 95 mInflater = LayoutInflater.from(context); 96 setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); 97 setBackgroundResource(R.drawable.widget_internal_focus_bg); 98 99 if (Utilities.ATLEAST_OREO) { 100 setExecutor(Executors.THREAD_POOL_EXECUTOR); 101 } 102 if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) { 103 setOnLightBackground(true); 104 } 105 } 106 107 @Override onLongClick(View view)108 public boolean onLongClick(View view) { 109 if (mIsScrollable) { 110 DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer(); 111 dragLayer.requestDisallowInterceptTouchEvent(false); 112 } 113 view.performLongClick(); 114 return true; 115 } 116 117 @Override getErrorView()118 protected View getErrorView() { 119 return mInflater.inflate(R.layout.appwidget_error, this, false); 120 } 121 122 @Override updateAppWidget(RemoteViews remoteViews)123 public void updateAppWidget(RemoteViews remoteViews) { 124 super.updateAppWidget(remoteViews); 125 126 // The provider info or the views might have changed. 127 checkIfAutoAdvance(); 128 129 // It is possible that widgets can receive updates while launcher is not in the foreground. 130 // Consequently, the widgets will be inflated for the orientation of the foreground activity 131 // (framework issue). On resuming, we ensure that any widgets are inflated for the current 132 // orientation. 133 mReinflateOnConfigChange = !isSameOrientation(); 134 } 135 isSameOrientation()136 private boolean isSameOrientation() { 137 return mLauncher.getResources().getConfiguration().orientation == 138 mLauncher.getOrientation(); 139 } 140 checkScrollableRecursively(ViewGroup viewGroup)141 private boolean checkScrollableRecursively(ViewGroup viewGroup) { 142 if (viewGroup instanceof AdapterView) { 143 return true; 144 } else { 145 for (int i=0; i < viewGroup.getChildCount(); i++) { 146 View child = viewGroup.getChildAt(i); 147 if (child instanceof ViewGroup) { 148 if (checkScrollableRecursively((ViewGroup) child)) { 149 return true; 150 } 151 } 152 } 153 } 154 return false; 155 } 156 onInterceptTouchEvent(MotionEvent ev)157 public boolean onInterceptTouchEvent(MotionEvent ev) { 158 // Just in case the previous long press hasn't been cleared, we make sure to start fresh 159 // on touch down. 160 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 161 mLongPressHelper.cancelLongPress(); 162 } 163 164 // Consume any touch events for ourselves after longpress is triggered 165 if (mLongPressHelper.hasPerformedLongPress()) { 166 mLongPressHelper.cancelLongPress(); 167 return true; 168 } 169 170 // Watch for longpress or stylus button press events at this level to 171 // make sure users can always pick up this widget 172 if (mStylusEventHelper.onMotionEvent(ev)) { 173 mLongPressHelper.cancelLongPress(); 174 return true; 175 } 176 177 switch (ev.getAction()) { 178 case MotionEvent.ACTION_DOWN: { 179 DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer(); 180 181 if (mIsScrollable) { 182 dragLayer.requestDisallowInterceptTouchEvent(true); 183 } 184 if (!mStylusEventHelper.inStylusButtonPressed()) { 185 mLongPressHelper.postCheckForLongPress(); 186 } 187 dragLayer.setTouchCompleteListener(this); 188 break; 189 } 190 191 case MotionEvent.ACTION_UP: 192 case MotionEvent.ACTION_CANCEL: 193 mLongPressHelper.cancelLongPress(); 194 break; 195 case MotionEvent.ACTION_MOVE: 196 if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) { 197 mLongPressHelper.cancelLongPress(); 198 } 199 break; 200 } 201 202 // Otherwise continue letting touch events fall through to children 203 return false; 204 } 205 onTouchEvent(MotionEvent ev)206 public boolean onTouchEvent(MotionEvent ev) { 207 // If the widget does not handle touch, then cancel 208 // long press when we release the touch 209 switch (ev.getAction()) { 210 case MotionEvent.ACTION_UP: 211 case MotionEvent.ACTION_CANCEL: 212 mLongPressHelper.cancelLongPress(); 213 break; 214 case MotionEvent.ACTION_MOVE: 215 if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) { 216 mLongPressHelper.cancelLongPress(); 217 } 218 break; 219 } 220 // We want to keep receiving though events to be able to cancel long press on ACTION_UP 221 return true; 222 } 223 224 @Override onAttachedToWindow()225 protected void onAttachedToWindow() { 226 super.onAttachedToWindow(); 227 mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 228 229 mIsAttachedToWindow = true; 230 checkIfAutoAdvance(); 231 } 232 233 @Override onDetachedFromWindow()234 protected void onDetachedFromWindow() { 235 super.onDetachedFromWindow(); 236 237 // We can't directly use isAttachedToWindow() here, as this is called before the internal 238 // state is updated. So isAttachedToWindow() will return true until next frame. 239 mIsAttachedToWindow = false; 240 checkIfAutoAdvance(); 241 } 242 243 @Override cancelLongPress()244 public void cancelLongPress() { 245 super.cancelLongPress(); 246 mLongPressHelper.cancelLongPress(); 247 } 248 249 @Override getAppWidgetInfo()250 public AppWidgetProviderInfo getAppWidgetInfo() { 251 AppWidgetProviderInfo info = super.getAppWidgetInfo(); 252 if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) { 253 throw new IllegalStateException("Launcher widget must have" 254 + " LauncherAppWidgetProviderInfo"); 255 } 256 return info; 257 } 258 259 @Override onTouchComplete()260 public void onTouchComplete() { 261 if (!mLongPressHelper.hasPerformedLongPress()) { 262 // If a long press has been performed, we don't want to clear the record of that since 263 // we still may be receiving a touch up which we want to intercept 264 mLongPressHelper.cancelLongPress(); 265 } 266 } 267 switchToErrorView()268 public void switchToErrorView() { 269 // Update the widget with 0 Layout id, to reset the view to error view. 270 updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0)); 271 } 272 273 @Override onLayout(boolean changed, int left, int top, int right, int bottom)274 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 275 try { 276 super.onLayout(changed, left, top, right, bottom); 277 } catch (final RuntimeException e) { 278 post(new Runnable() { 279 @Override 280 public void run() { 281 switchToErrorView(); 282 } 283 }); 284 } 285 286 mIsScrollable = checkScrollableRecursively(this); 287 } 288 289 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)290 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 291 super.onInitializeAccessibilityNodeInfo(info); 292 info.setClassName(getClass().getName()); 293 } 294 295 @Override onWindowVisibilityChanged(int visibility)296 protected void onWindowVisibilityChanged(int visibility) { 297 super.onWindowVisibilityChanged(visibility); 298 maybeRegisterAutoAdvance(); 299 } 300 checkIfAutoAdvance()301 private void checkIfAutoAdvance() { 302 boolean isAutoAdvance = false; 303 Advanceable target = getAdvanceable(); 304 if (target != null) { 305 isAutoAdvance = true; 306 target.fyiWillBeAdvancedByHostKThx(); 307 } 308 309 boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0; 310 if (isAutoAdvance != wasAutoAdvance) { 311 if (isAutoAdvance) { 312 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true); 313 } else { 314 sAutoAdvanceWidgetIds.delete(getAppWidgetId()); 315 } 316 maybeRegisterAutoAdvance(); 317 } 318 } 319 getAdvanceable()320 private Advanceable getAdvanceable() { 321 AppWidgetProviderInfo info = getAppWidgetInfo(); 322 if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) { 323 return null; 324 } 325 View v = findViewById(info.autoAdvanceViewId); 326 return (v instanceof Advanceable) ? (Advanceable) v : null; 327 } 328 maybeRegisterAutoAdvance()329 private void maybeRegisterAutoAdvance() { 330 Handler handler = getHandler(); 331 boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null 332 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0); 333 if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) { 334 mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance; 335 if (mAutoAdvanceRunnable == null) { 336 mAutoAdvanceRunnable = this::runAutoAdvance; 337 } 338 339 handler.removeCallbacks(mAutoAdvanceRunnable); 340 scheduleNextAdvance(); 341 } 342 } 343 scheduleNextAdvance()344 private void scheduleNextAdvance() { 345 if (!mIsAutoAdvanceRegistered) { 346 return; 347 } 348 long now = SystemClock.uptimeMillis(); 349 long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) + 350 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()); 351 Handler handler = getHandler(); 352 if (handler != null) { 353 handler.postAtTime(mAutoAdvanceRunnable, advanceTime); 354 } 355 } 356 runAutoAdvance()357 private void runAutoAdvance() { 358 Advanceable target = getAdvanceable(); 359 if (target != null) { 360 target.advance(); 361 } 362 scheduleNextAdvance(); 363 } 364 setScaleToFit(float scale)365 public void setScaleToFit(float scale) { 366 mScaleToFit = scale; 367 setScaleX(scale); 368 setScaleY(scale); 369 } 370 getScaleToFit()371 public float getScaleToFit() { 372 return mScaleToFit; 373 } 374 setTranslationForCentering(float x, float y)375 public void setTranslationForCentering(float x, float y) { 376 mTranslationForCentering.set(x, y); 377 setTranslationX(x); 378 setTranslationY(y); 379 } 380 getTranslationForCentering()381 public PointF getTranslationForCentering() { 382 return mTranslationForCentering; 383 } 384 385 @Override onConfigurationChanged(Configuration newConfig)386 protected void onConfigurationChanged(Configuration newConfig) { 387 super.onConfigurationChanged(newConfig); 388 389 // Only reinflate when the final configuration is same as the required configuration 390 if (mReinflateOnConfigChange && isSameOrientation()) { 391 mReinflateOnConfigChange = false; 392 reInflate(); 393 } 394 } 395 reInflate()396 public void reInflate() { 397 if (!isAttachedToWindow()) { 398 return; 399 } 400 LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); 401 // Remove and rebind the current widget (which was inflated in the wrong 402 // orientation), but don't delete it from the database 403 mLauncher.removeItem(this, info, false /* deleteFromDb */); 404 mLauncher.bindAppWidget(info); 405 } 406 407 @Override shouldAllowDirectClick()408 protected boolean shouldAllowDirectClick() { 409 if (getTag() instanceof ItemInfo) { 410 ItemInfo item = (ItemInfo) getTag(); 411 return item.spanX == 1 && item.spanY == 1; 412 } 413 return false; 414 } 415 } 416