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; 18 19 import android.appwidget.AppWidgetHostView; 20 import android.appwidget.AppWidgetProviderInfo; 21 import android.content.Context; 22 import android.graphics.PointF; 23 import android.graphics.Rect; 24 import android.os.Handler; 25 import android.os.SystemClock; 26 import android.util.Log; 27 import android.util.SparseBooleanArray; 28 import android.view.KeyEvent; 29 import android.view.LayoutInflater; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewConfiguration; 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 com.android.launcher3.dragndrop.DragLayer; 41 import com.android.launcher3.dragndrop.DragLayer.TouchCompleteListener; 42 43 import java.lang.reflect.Method; 44 import java.util.ArrayList; 45 import java.util.concurrent.Executor; 46 47 /** 48 * {@inheritDoc} 49 */ 50 public class LauncherAppWidgetHostView extends AppWidgetHostView 51 implements TouchCompleteListener, View.OnLongClickListener { 52 53 private static final String TAG = "LauncherWidgetHostView"; 54 55 // Related to the auto-advancing of widgets 56 private static final long ADVANCE_INTERVAL = 20000; 57 private static final long ADVANCE_STAGGER = 250; 58 59 // Maintains a list of widget ids which are supposed to be auto advanced. 60 private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray(); 61 62 protected final LayoutInflater mInflater; 63 64 private final CheckLongPressHelper mLongPressHelper; 65 private final StylusEventHelper mStylusEventHelper; 66 private final Context mContext; 67 68 @ViewDebug.ExportedProperty(category = "launcher") 69 private int mPreviousOrientation; 70 71 private float mSlop; 72 73 @ViewDebug.ExportedProperty(category = "launcher") 74 private boolean mChildrenFocused; 75 76 private boolean mIsScrollable; 77 private boolean mIsAttachedToWindow; 78 private boolean mIsAutoAdvanceRegistered; 79 private Runnable mAutoAdvanceRunnable; 80 81 /** 82 * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY. 83 */ 84 private float mScaleToFit = 1f; 85 86 /** 87 * The translation values to center the widget within its cellspans. 88 */ 89 private final PointF mTranslationForCentering = new PointF(0, 0); 90 91 public LauncherAppWidgetHostView(Context context) { 92 super(context); 93 mContext = context; 94 mLongPressHelper = new CheckLongPressHelper(this, this); 95 mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this); 96 mInflater = LayoutInflater.from(context); 97 setAccessibilityDelegate(Launcher.getLauncher(context).getAccessibilityDelegate()); 98 setBackgroundResource(R.drawable.widget_internal_focus_bg); 99 100 if (Utilities.isAtLeastO()) { 101 try { 102 Method asyncMethod = AppWidgetHostView.class 103 .getMethod("setExecutor", Executor.class); 104 asyncMethod.invoke(this, Utilities.THREAD_POOL_EXECUTOR); 105 } catch (Exception e) { 106 Log.e(TAG, "Unable to set async executor", e); 107 } 108 } 109 } 110 111 @Override 112 public boolean onLongClick(View view) { 113 if (mIsScrollable) { 114 DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer(); 115 dragLayer.requestDisallowInterceptTouchEvent(false); 116 } 117 view.performLongClick(); 118 return true; 119 } 120 121 @Override 122 protected View getErrorView() { 123 return mInflater.inflate(R.layout.appwidget_error, this, false); 124 } 125 126 public void updateLastInflationOrientation() { 127 mPreviousOrientation = mContext.getResources().getConfiguration().orientation; 128 } 129 130 @Override 131 public void updateAppWidget(RemoteViews remoteViews) { 132 // Store the orientation in which the widget was inflated 133 updateLastInflationOrientation(); 134 super.updateAppWidget(remoteViews); 135 136 // The provider info or the views might have changed. 137 checkIfAutoAdvance(); 138 } 139 140 private boolean checkScrollableRecursively(ViewGroup viewGroup) { 141 if (viewGroup instanceof AdapterView) { 142 return true; 143 } else { 144 for (int i=0; i < viewGroup.getChildCount(); i++) { 145 View child = viewGroup.getChildAt(i); 146 if (child instanceof ViewGroup) { 147 if (checkScrollableRecursively((ViewGroup) child)) { 148 return true; 149 } 150 } 151 } 152 } 153 return false; 154 } 155 156 public boolean isReinflateRequired() { 157 // Re-inflate is required if the orientation has changed since last inflated. 158 int orientation = mContext.getResources().getConfiguration().orientation; 159 if (mPreviousOrientation != orientation) { 160 return true; 161 } 162 return false; 163 } 164 165 public boolean onInterceptTouchEvent(MotionEvent ev) { 166 // Just in case the previous long press hasn't been cleared, we make sure to start fresh 167 // on touch down. 168 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 169 mLongPressHelper.cancelLongPress(); 170 } 171 172 // Consume any touch events for ourselves after longpress is triggered 173 if (mLongPressHelper.hasPerformedLongPress()) { 174 mLongPressHelper.cancelLongPress(); 175 return true; 176 } 177 178 // Watch for longpress or stylus button press events at this level to 179 // make sure users can always pick up this widget 180 if (mStylusEventHelper.onMotionEvent(ev)) { 181 mLongPressHelper.cancelLongPress(); 182 return true; 183 } 184 185 switch (ev.getAction()) { 186 case MotionEvent.ACTION_DOWN: { 187 DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer(); 188 189 if (mIsScrollable) { 190 dragLayer.requestDisallowInterceptTouchEvent(true); 191 } 192 if (!mStylusEventHelper.inStylusButtonPressed()) { 193 mLongPressHelper.postCheckForLongPress(); 194 } 195 dragLayer.setTouchCompleteListener(this); 196 break; 197 } 198 199 case MotionEvent.ACTION_UP: 200 case MotionEvent.ACTION_CANCEL: 201 mLongPressHelper.cancelLongPress(); 202 break; 203 case MotionEvent.ACTION_MOVE: 204 if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) { 205 mLongPressHelper.cancelLongPress(); 206 } 207 break; 208 } 209 210 // Otherwise continue letting touch events fall through to children 211 return false; 212 } 213 214 public boolean onTouchEvent(MotionEvent ev) { 215 // If the widget does not handle touch, then cancel 216 // long press when we release the touch 217 switch (ev.getAction()) { 218 case MotionEvent.ACTION_UP: 219 case MotionEvent.ACTION_CANCEL: 220 mLongPressHelper.cancelLongPress(); 221 break; 222 case MotionEvent.ACTION_MOVE: 223 if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) { 224 mLongPressHelper.cancelLongPress(); 225 } 226 break; 227 } 228 return false; 229 } 230 231 @Override 232 protected void onAttachedToWindow() { 233 super.onAttachedToWindow(); 234 mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 235 236 mIsAttachedToWindow = true; 237 checkIfAutoAdvance(); 238 } 239 240 @Override 241 protected void onDetachedFromWindow() { 242 super.onDetachedFromWindow(); 243 244 // We can't directly use isAttachedToWindow() here, as this is called before the internal 245 // state is updated. So isAttachedToWindow() will return true until next frame. 246 mIsAttachedToWindow = false; 247 checkIfAutoAdvance(); 248 } 249 250 @Override 251 public void cancelLongPress() { 252 super.cancelLongPress(); 253 mLongPressHelper.cancelLongPress(); 254 } 255 256 @Override 257 public AppWidgetProviderInfo getAppWidgetInfo() { 258 AppWidgetProviderInfo info = super.getAppWidgetInfo(); 259 if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) { 260 throw new IllegalStateException("Launcher widget must have" 261 + " LauncherAppWidgetProviderInfo"); 262 } 263 return info; 264 } 265 266 @Override 267 public void onTouchComplete() { 268 if (!mLongPressHelper.hasPerformedLongPress()) { 269 // If a long press has been performed, we don't want to clear the record of that since 270 // we still may be receiving a touch up which we want to intercept 271 mLongPressHelper.cancelLongPress(); 272 } 273 } 274 275 @Override 276 public int getDescendantFocusability() { 277 return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS 278 : ViewGroup.FOCUS_BLOCK_DESCENDANTS; 279 } 280 281 @Override 282 public boolean dispatchKeyEvent(KeyEvent event) { 283 if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE 284 && event.getAction() == KeyEvent.ACTION_UP) { 285 mChildrenFocused = false; 286 requestFocus(); 287 return true; 288 } 289 return super.dispatchKeyEvent(event); 290 } 291 292 @Override 293 public boolean onKeyDown(int keyCode, KeyEvent event) { 294 if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { 295 event.startTracking(); 296 return true; 297 } 298 return super.onKeyDown(keyCode, event); 299 } 300 301 @Override 302 public boolean onKeyUp(int keyCode, KeyEvent event) { 303 if (event.isTracking()) { 304 if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { 305 mChildrenFocused = true; 306 ArrayList<View> focusableChildren = getFocusables(FOCUS_FORWARD); 307 focusableChildren.remove(this); 308 int childrenCount = focusableChildren.size(); 309 switch (childrenCount) { 310 case 0: 311 mChildrenFocused = false; 312 break; 313 case 1: { 314 if (getTag() instanceof ItemInfo) { 315 ItemInfo item = (ItemInfo) getTag(); 316 if (item.spanX == 1 && item.spanY == 1) { 317 focusableChildren.get(0).performClick(); 318 mChildrenFocused = false; 319 return true; 320 } 321 } 322 // continue; 323 } 324 default: 325 focusableChildren.get(0).requestFocus(); 326 return true; 327 } 328 } 329 } 330 return super.onKeyUp(keyCode, event); 331 } 332 333 @Override 334 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 335 if (gainFocus) { 336 mChildrenFocused = false; 337 dispatchChildFocus(false); 338 } 339 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 340 } 341 342 @Override 343 public void requestChildFocus(View child, View focused) { 344 super.requestChildFocus(child, focused); 345 dispatchChildFocus(mChildrenFocused && focused != null); 346 if (focused != null) { 347 focused.setFocusableInTouchMode(false); 348 } 349 } 350 351 @Override 352 public void clearChildFocus(View child) { 353 super.clearChildFocus(child); 354 dispatchChildFocus(false); 355 } 356 357 @Override 358 public boolean dispatchUnhandledMove(View focused, int direction) { 359 return mChildrenFocused; 360 } 361 362 private void dispatchChildFocus(boolean childIsFocused) { 363 // The host view's background changes when selected, to indicate the focus is inside. 364 setSelected(childIsFocused); 365 } 366 367 public void switchToErrorView() { 368 // Update the widget with 0 Layout id, to reset the view to error view. 369 updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0)); 370 } 371 372 @Override 373 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 374 try { 375 super.onLayout(changed, left, top, right, bottom); 376 } catch (final RuntimeException e) { 377 post(new Runnable() { 378 @Override 379 public void run() { 380 switchToErrorView(); 381 } 382 }); 383 } 384 385 mIsScrollable = checkScrollableRecursively(this); 386 } 387 388 @Override 389 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 390 super.onInitializeAccessibilityNodeInfo(info); 391 info.setClassName(getClass().getName()); 392 } 393 394 @Override 395 protected void onWindowVisibilityChanged(int visibility) { 396 super.onWindowVisibilityChanged(visibility); 397 maybeRegisterAutoAdvance(); 398 } 399 400 private void checkIfAutoAdvance() { 401 boolean isAutoAdvance = false; 402 Advanceable target = getAdvanceable(); 403 if (target != null) { 404 isAutoAdvance = true; 405 target.fyiWillBeAdvancedByHostKThx(); 406 } 407 408 boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0; 409 if (isAutoAdvance != wasAutoAdvance) { 410 if (isAutoAdvance) { 411 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true); 412 } else { 413 sAutoAdvanceWidgetIds.delete(getAppWidgetId()); 414 } 415 maybeRegisterAutoAdvance(); 416 } 417 } 418 419 private Advanceable getAdvanceable() { 420 AppWidgetProviderInfo info = getAppWidgetInfo(); 421 if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) { 422 return null; 423 } 424 View v = findViewById(info.autoAdvanceViewId); 425 return (v instanceof Advanceable) ? (Advanceable) v : null; 426 } 427 428 private void maybeRegisterAutoAdvance() { 429 Handler handler = getHandler(); 430 boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null 431 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0); 432 if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) { 433 mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance; 434 if (mAutoAdvanceRunnable == null) { 435 mAutoAdvanceRunnable = new Runnable() { 436 @Override 437 public void run() { 438 runAutoAdvance(); 439 } 440 }; 441 } 442 443 handler.removeCallbacks(mAutoAdvanceRunnable); 444 scheduleNextAdvance(); 445 } 446 } 447 448 private void scheduleNextAdvance() { 449 if (!mIsAutoAdvanceRegistered) { 450 return; 451 } 452 long now = SystemClock.uptimeMillis(); 453 long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) + 454 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()); 455 Handler handler = getHandler(); 456 if (handler != null) { 457 handler.postAtTime(mAutoAdvanceRunnable, advanceTime); 458 } 459 } 460 461 private void runAutoAdvance() { 462 Advanceable target = getAdvanceable(); 463 if (target != null) { 464 target.advance(); 465 } 466 scheduleNextAdvance(); 467 } 468 469 public void setScaleToFit(float scale) { 470 mScaleToFit = scale; 471 setScaleX(scale); 472 setScaleY(scale); 473 } 474 475 public float getScaleToFit() { 476 return mScaleToFit; 477 } 478 479 public void setTranslationForCentering(float x, float y) { 480 mTranslationForCentering.set(x, y); 481 setTranslationX(x); 482 setTranslationY(y); 483 } 484 485 public PointF getTranslationForCentering() { 486 return mTranslationForCentering; 487 } 488 } 489