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.os.Handler; 23 import android.os.SystemClock; 24 import android.util.SparseBooleanArray; 25 import android.view.LayoutInflater; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewDebug; 29 import android.view.ViewGroup; 30 import android.view.accessibility.AccessibilityNodeInfo; 31 import android.widget.AdapterView; 32 import android.widget.Advanceable; 33 import android.widget.RemoteViews; 34 35 import com.android.launcher3.CheckLongPressHelper; 36 import com.android.launcher3.Launcher; 37 import com.android.launcher3.LauncherAppWidgetProviderInfo; 38 import com.android.launcher3.R; 39 import com.android.launcher3.Utilities; 40 import com.android.launcher3.dragndrop.DragLayer; 41 import com.android.launcher3.model.data.ItemInfo; 42 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 43 import com.android.launcher3.util.Executors; 44 import com.android.launcher3.util.Themes; 45 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener; 46 47 /** 48 * {@inheritDoc} 49 */ 50 public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView 51 implements TouchCompleteListener, View.OnLongClickListener { 52 53 // Related to the auto-advancing of widgets 54 private static final long ADVANCE_INTERVAL = 20000; 55 private static final long ADVANCE_STAGGER = 250; 56 57 // Maintains a list of widget ids which are supposed to be auto advanced. 58 private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray(); 59 60 protected final LayoutInflater mInflater; 61 62 private final CheckLongPressHelper mLongPressHelper; 63 protected final Launcher mLauncher; 64 65 @ViewDebug.ExportedProperty(category = "launcher") 66 private boolean mReinflateOnConfigChange; 67 68 private boolean mIsScrollable; 69 private boolean mIsAttachedToWindow; 70 private boolean mIsAutoAdvanceRegistered; 71 private Runnable mAutoAdvanceRunnable; 72 73 74 LauncherAppWidgetHostView(Context context)75 public LauncherAppWidgetHostView(Context context) { 76 super(context); 77 mLauncher = Launcher.getLauncher(context); 78 mLongPressHelper = new CheckLongPressHelper(this, this); 79 mInflater = LayoutInflater.from(context); 80 setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); 81 setBackgroundResource(R.drawable.widget_internal_focus_bg); 82 83 if (Utilities.ATLEAST_OREO) { 84 setExecutor(Executors.THREAD_POOL_EXECUTOR); 85 } 86 if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) { 87 setOnLightBackground(true); 88 } 89 } 90 91 @Override onLongClick(View view)92 public boolean onLongClick(View view) { 93 if (mIsScrollable) { 94 DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer(); 95 dragLayer.requestDisallowInterceptTouchEvent(false); 96 } 97 view.performLongClick(); 98 return true; 99 } 100 101 @Override getErrorView()102 protected View getErrorView() { 103 return mInflater.inflate(R.layout.appwidget_error, this, false); 104 } 105 106 @Override updateAppWidget(RemoteViews remoteViews)107 public void updateAppWidget(RemoteViews remoteViews) { 108 super.updateAppWidget(remoteViews); 109 110 // The provider info or the views might have changed. 111 checkIfAutoAdvance(); 112 113 // It is possible that widgets can receive updates while launcher is not in the foreground. 114 // Consequently, the widgets will be inflated for the orientation of the foreground activity 115 // (framework issue). On resuming, we ensure that any widgets are inflated for the current 116 // orientation. 117 mReinflateOnConfigChange = !isSameOrientation(); 118 } 119 isSameOrientation()120 private boolean isSameOrientation() { 121 return mLauncher.getResources().getConfiguration().orientation == 122 mLauncher.getOrientation(); 123 } 124 checkScrollableRecursively(ViewGroup viewGroup)125 private boolean checkScrollableRecursively(ViewGroup viewGroup) { 126 if (viewGroup instanceof AdapterView) { 127 return true; 128 } else { 129 for (int i=0; i < viewGroup.getChildCount(); i++) { 130 View child = viewGroup.getChildAt(i); 131 if (child instanceof ViewGroup) { 132 if (checkScrollableRecursively((ViewGroup) child)) { 133 return true; 134 } 135 } 136 } 137 } 138 return false; 139 } 140 onInterceptTouchEvent(MotionEvent ev)141 public boolean onInterceptTouchEvent(MotionEvent ev) { 142 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 143 DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer(); 144 if (mIsScrollable) { 145 dragLayer.requestDisallowInterceptTouchEvent(true); 146 } 147 dragLayer.setTouchCompleteListener(this); 148 } 149 mLongPressHelper.onTouchEvent(ev); 150 return mLongPressHelper.hasPerformedLongPress(); 151 } 152 onTouchEvent(MotionEvent ev)153 public boolean onTouchEvent(MotionEvent ev) { 154 mLongPressHelper.onTouchEvent(ev); 155 // We want to keep receiving though events to be able to cancel long press on ACTION_UP 156 return true; 157 } 158 159 @Override onAttachedToWindow()160 protected void onAttachedToWindow() { 161 super.onAttachedToWindow(); 162 163 mIsAttachedToWindow = true; 164 checkIfAutoAdvance(); 165 } 166 167 @Override onDetachedFromWindow()168 protected void onDetachedFromWindow() { 169 super.onDetachedFromWindow(); 170 171 // We can't directly use isAttachedToWindow() here, as this is called before the internal 172 // state is updated. So isAttachedToWindow() will return true until next frame. 173 mIsAttachedToWindow = false; 174 checkIfAutoAdvance(); 175 } 176 177 @Override cancelLongPress()178 public void cancelLongPress() { 179 super.cancelLongPress(); 180 mLongPressHelper.cancelLongPress(); 181 } 182 183 @Override getAppWidgetInfo()184 public AppWidgetProviderInfo getAppWidgetInfo() { 185 AppWidgetProviderInfo info = super.getAppWidgetInfo(); 186 if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) { 187 throw new IllegalStateException("Launcher widget must have" 188 + " LauncherAppWidgetProviderInfo"); 189 } 190 return info; 191 } 192 193 @Override onTouchComplete()194 public void onTouchComplete() { 195 if (!mLongPressHelper.hasPerformedLongPress()) { 196 // If a long press has been performed, we don't want to clear the record of that since 197 // we still may be receiving a touch up which we want to intercept 198 mLongPressHelper.cancelLongPress(); 199 } 200 } 201 switchToErrorView()202 public void switchToErrorView() { 203 // Update the widget with 0 Layout id, to reset the view to error view. 204 updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0)); 205 } 206 207 @Override onLayout(boolean changed, int left, int top, int right, int bottom)208 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 209 try { 210 super.onLayout(changed, left, top, right, bottom); 211 } catch (final RuntimeException e) { 212 post(new Runnable() { 213 @Override 214 public void run() { 215 switchToErrorView(); 216 } 217 }); 218 } 219 220 mIsScrollable = checkScrollableRecursively(this); 221 } 222 223 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)224 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 225 super.onInitializeAccessibilityNodeInfo(info); 226 info.setClassName(getClass().getName()); 227 } 228 229 @Override onWindowVisibilityChanged(int visibility)230 protected void onWindowVisibilityChanged(int visibility) { 231 super.onWindowVisibilityChanged(visibility); 232 maybeRegisterAutoAdvance(); 233 } 234 checkIfAutoAdvance()235 private void checkIfAutoAdvance() { 236 boolean isAutoAdvance = false; 237 Advanceable target = getAdvanceable(); 238 if (target != null) { 239 isAutoAdvance = true; 240 target.fyiWillBeAdvancedByHostKThx(); 241 } 242 243 boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0; 244 if (isAutoAdvance != wasAutoAdvance) { 245 if (isAutoAdvance) { 246 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true); 247 } else { 248 sAutoAdvanceWidgetIds.delete(getAppWidgetId()); 249 } 250 maybeRegisterAutoAdvance(); 251 } 252 } 253 getAdvanceable()254 private Advanceable getAdvanceable() { 255 AppWidgetProviderInfo info = getAppWidgetInfo(); 256 if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) { 257 return null; 258 } 259 View v = findViewById(info.autoAdvanceViewId); 260 return (v instanceof Advanceable) ? (Advanceable) v : null; 261 } 262 maybeRegisterAutoAdvance()263 private void maybeRegisterAutoAdvance() { 264 Handler handler = getHandler(); 265 boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null 266 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0); 267 if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) { 268 mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance; 269 if (mAutoAdvanceRunnable == null) { 270 mAutoAdvanceRunnable = this::runAutoAdvance; 271 } 272 273 handler.removeCallbacks(mAutoAdvanceRunnable); 274 scheduleNextAdvance(); 275 } 276 } 277 scheduleNextAdvance()278 private void scheduleNextAdvance() { 279 if (!mIsAutoAdvanceRegistered) { 280 return; 281 } 282 long now = SystemClock.uptimeMillis(); 283 long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) + 284 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()); 285 Handler handler = getHandler(); 286 if (handler != null) { 287 handler.postAtTime(mAutoAdvanceRunnable, advanceTime); 288 } 289 } 290 runAutoAdvance()291 private void runAutoAdvance() { 292 Advanceable target = getAdvanceable(); 293 if (target != null) { 294 target.advance(); 295 } 296 scheduleNextAdvance(); 297 } 298 299 @Override onConfigurationChanged(Configuration newConfig)300 protected void onConfigurationChanged(Configuration newConfig) { 301 super.onConfigurationChanged(newConfig); 302 303 // Only reinflate when the final configuration is same as the required configuration 304 if (mReinflateOnConfigChange && isSameOrientation()) { 305 mReinflateOnConfigChange = false; 306 reInflate(); 307 } 308 } 309 reInflate()310 public void reInflate() { 311 if (!isAttachedToWindow()) { 312 return; 313 } 314 LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); 315 // Remove and rebind the current widget (which was inflated in the wrong 316 // orientation), but don't delete it from the database 317 mLauncher.removeItem(this, info, false /* deleteFromDb */); 318 mLauncher.bindAppWidget(info); 319 } 320 321 @Override shouldAllowDirectClick()322 protected boolean shouldAllowDirectClick() { 323 if (getTag() instanceof ItemInfo) { 324 ItemInfo item = (ItemInfo) getTag(); 325 return item.spanX == 1 && item.spanY == 1; 326 } 327 return false; 328 } 329 } 330