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 static android.app.Activity.RESULT_CANCELED; 20 21 import android.appwidget.AppWidgetHost; 22 import android.appwidget.AppWidgetHostView; 23 import android.appwidget.AppWidgetManager; 24 import android.appwidget.AppWidgetProviderInfo; 25 import android.content.ActivityNotFoundException; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.util.SparseArray; 31 import android.widget.Toast; 32 33 import androidx.annotation.Nullable; 34 35 import com.android.launcher3.BaseActivity; 36 import com.android.launcher3.BaseDraggingActivity; 37 import com.android.launcher3.LauncherAppState; 38 import com.android.launcher3.R; 39 import com.android.launcher3.Utilities; 40 import com.android.launcher3.model.WidgetsModel; 41 import com.android.launcher3.model.data.ItemInfo; 42 import com.android.launcher3.testing.TestLogging; 43 import com.android.launcher3.testing.TestProtocol; 44 import com.android.launcher3.widget.custom.CustomWidgetManager; 45 46 import java.util.ArrayList; 47 import java.util.function.IntConsumer; 48 49 50 /** 51 * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView} 52 * which correctly captures all long-press events. This ensures that users can 53 * always pick up and move widgets. 54 */ 55 public class LauncherAppWidgetHost extends AppWidgetHost { 56 57 private static final int FLAG_LISTENING = 1; 58 private static final int FLAG_STATE_IS_NORMAL = 1 << 1; 59 private static final int FLAG_ACTIVITY_STARTED = 1 << 2; 60 private static final int FLAG_ACTIVITY_RESUMED = 1 << 3; 61 private static final int FLAGS_SHOULD_LISTEN = 62 FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED | FLAG_ACTIVITY_RESUMED; 63 // TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden 64 private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle"; 65 // TODO(b/191735836): Replace with SplashScreen.SPLASH_SCREEN_STYLE_EMPTY when un-hidden 66 private static final int SPLASH_SCREEN_STYLE_EMPTY = 0; 67 68 public static final int APPWIDGET_HOST_ID = 1024; 69 70 private final ArrayList<ProviderChangedListener> mProviderChangeListeners = new ArrayList<>(); 71 private final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>(); 72 private final SparseArray<PendingAppWidgetHostView> mPendingViews = new SparseArray<>(); 73 74 private final Context mContext; 75 private int mFlags = FLAG_STATE_IS_NORMAL; 76 77 private IntConsumer mAppWidgetRemovedCallback = null; 78 79 LauncherAppWidgetHost(Context context)80 public LauncherAppWidgetHost(Context context) { 81 this(context, null); 82 } 83 LauncherAppWidgetHost(Context context, IntConsumer appWidgetRemovedCallback)84 public LauncherAppWidgetHost(Context context, 85 IntConsumer appWidgetRemovedCallback) { 86 super(context, APPWIDGET_HOST_ID); 87 mContext = context; 88 mAppWidgetRemovedCallback = appWidgetRemovedCallback; 89 } 90 91 @Override onCreateView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget)92 protected LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId, 93 AppWidgetProviderInfo appWidget) { 94 final LauncherAppWidgetHostView view; 95 if (mPendingViews.get(appWidgetId) != null) { 96 view = mPendingViews.get(appWidgetId); 97 mPendingViews.remove(appWidgetId); 98 } else { 99 view = new LauncherAppWidgetHostView(context); 100 } 101 mViews.put(appWidgetId, view); 102 return view; 103 } 104 105 @Override startListening()106 public void startListening() { 107 if (WidgetsModel.GO_DISABLE_WIDGETS) { 108 return; 109 } 110 mFlags |= FLAG_LISTENING; 111 try { 112 super.startListening(); 113 } catch (Exception e) { 114 if (!Utilities.isBinderSizeError(e)) { 115 throw new RuntimeException(e); 116 } 117 // We're willing to let this slide. The exception is being caused by the list of 118 // RemoteViews which is being passed back. The startListening relationship will 119 // have been established by this point, and we will end up populating the 120 // widgets upon bind anyway. See issue 14255011 for more context. 121 } 122 123 // We go in reverse order and inflate any deferred widget 124 for (int i = mViews.size() - 1; i >= 0; i--) { 125 LauncherAppWidgetHostView view = mViews.valueAt(i); 126 if (view instanceof DeferredAppWidgetHostView) { 127 view.reInflate(); 128 } 129 } 130 } 131 132 @Override stopListening()133 public void stopListening() { 134 if (WidgetsModel.GO_DISABLE_WIDGETS) { 135 return; 136 } 137 mFlags &= ~FLAG_LISTENING; 138 super.stopListening(); 139 } 140 isListening()141 public boolean isListening() { 142 return (mFlags & FLAG_LISTENING) != 0; 143 } 144 145 /** 146 * Sets or unsets a flag the can change whether the widget host should be in the listening 147 * state. 148 */ setShouldListenFlag(int flag, boolean on)149 private void setShouldListenFlag(int flag, boolean on) { 150 if (on) { 151 mFlags |= flag; 152 } else { 153 mFlags &= ~flag; 154 } 155 156 final boolean listening = isListening(); 157 if (!listening && (mFlags & FLAGS_SHOULD_LISTEN) == FLAGS_SHOULD_LISTEN) { 158 // Postpone starting listening until all flags are on. 159 startListening(); 160 } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) { 161 // Postpone stopping listening until the activity is stopped. 162 stopListening(); 163 } 164 } 165 166 /** 167 * Registers an "entering/leaving Normal state" event. 168 */ setStateIsNormal(boolean isNormal)169 public void setStateIsNormal(boolean isNormal) { 170 setShouldListenFlag(FLAG_STATE_IS_NORMAL, isNormal); 171 } 172 173 /** 174 * Registers an "activity started/stopped" event. 175 */ setActivityStarted(boolean isStarted)176 public void setActivityStarted(boolean isStarted) { 177 setShouldListenFlag(FLAG_ACTIVITY_STARTED, isStarted); 178 } 179 180 /** 181 * Registers an "activity paused/resumed" event. 182 */ setActivityResumed(boolean isResumed)183 public void setActivityResumed(boolean isResumed) { 184 setShouldListenFlag(FLAG_ACTIVITY_RESUMED, isResumed); 185 } 186 187 @Override allocateAppWidgetId()188 public int allocateAppWidgetId() { 189 if (WidgetsModel.GO_DISABLE_WIDGETS) { 190 return AppWidgetManager.INVALID_APPWIDGET_ID; 191 } 192 193 return super.allocateAppWidgetId(); 194 } 195 addProviderChangeListener(ProviderChangedListener callback)196 public void addProviderChangeListener(ProviderChangedListener callback) { 197 mProviderChangeListeners.add(callback); 198 } 199 removeProviderChangeListener(ProviderChangedListener callback)200 public void removeProviderChangeListener(ProviderChangedListener callback) { 201 mProviderChangeListeners.remove(callback); 202 } 203 onProvidersChanged()204 protected void onProvidersChanged() { 205 if (!mProviderChangeListeners.isEmpty()) { 206 for (ProviderChangedListener callback : new ArrayList<>(mProviderChangeListeners)) { 207 callback.notifyWidgetProvidersChanged(); 208 } 209 } 210 } 211 addPendingView(int appWidgetId, PendingAppWidgetHostView view)212 public void addPendingView(int appWidgetId, PendingAppWidgetHostView view) { 213 mPendingViews.put(appWidgetId, view); 214 } 215 createView(Context context, int appWidgetId, LauncherAppWidgetProviderInfo appWidget)216 public AppWidgetHostView createView(Context context, int appWidgetId, 217 LauncherAppWidgetProviderInfo appWidget) { 218 if (appWidget.isCustomWidget()) { 219 LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context); 220 lahv.setAppWidget(0, appWidget); 221 CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv); 222 return lahv; 223 } else if ((mFlags & FLAG_LISTENING) == 0) { 224 DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context); 225 view.setAppWidget(appWidgetId, appWidget); 226 mViews.put(appWidgetId, view); 227 return view; 228 } else { 229 try { 230 return super.createView(context, appWidgetId, appWidget); 231 } catch (Exception e) { 232 if (!Utilities.isBinderSizeError(e)) { 233 throw new RuntimeException(e); 234 } 235 236 // If the exception was thrown while fetching the remote views, let the view stay. 237 // This will ensure that if the widget posts a valid update later, the view 238 // will update. 239 LauncherAppWidgetHostView view = mViews.get(appWidgetId); 240 if (view == null) { 241 view = onCreateView(mContext, appWidgetId, appWidget); 242 } 243 view.setAppWidget(appWidgetId, appWidget); 244 view.switchToErrorView(); 245 return view; 246 } 247 } 248 } 249 250 /** 251 * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk. 252 */ 253 @Override onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget)254 protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) { 255 LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo( 256 mContext, appWidget); 257 super.onProviderChanged(appWidgetId, info); 258 // The super method updates the dimensions of the providerInfo. Update the 259 // launcher spans accordingly. 260 info.initSpans(mContext, LauncherAppState.getIDP(mContext)); 261 } 262 263 /** 264 * Called on an appWidget is removed for a widgetId 265 * 266 * @param appWidgetId TODO: make this override when SDK is updated 267 */ onAppWidgetRemoved(int appWidgetId)268 public void onAppWidgetRemoved(int appWidgetId) { 269 if (mAppWidgetRemovedCallback == null) { 270 return; 271 } 272 mAppWidgetRemovedCallback.accept(appWidgetId); 273 } 274 275 @Override deleteAppWidgetId(int appWidgetId)276 public void deleteAppWidgetId(int appWidgetId) { 277 super.deleteAppWidgetId(appWidgetId); 278 mViews.remove(appWidgetId); 279 } 280 281 @Override clearViews()282 public void clearViews() { 283 super.clearViews(); 284 mViews.clear(); 285 } 286 startBindFlow(BaseActivity activity, int appWidgetId, AppWidgetProviderInfo info, int requestCode)287 public void startBindFlow(BaseActivity activity, 288 int appWidgetId, AppWidgetProviderInfo info, int requestCode) { 289 290 if (WidgetsModel.GO_DISABLE_WIDGETS) { 291 sendActionCancelled(activity, requestCode); 292 return; 293 } 294 295 Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND) 296 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) 297 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider) 298 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.getProfile()); 299 // TODO: we need to make sure that this accounts for the options bundle. 300 // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options); 301 activity.startActivityForResult(intent, requestCode); 302 } 303 304 /** 305 * Launches an app widget's configuration activity. 306 * @param activity The activity from which to launch the configuration activity 307 * @param widgetId The id of the bound app widget to be configured 308 * @param requestCode An optional request code to be returned with the result 309 */ startConfigActivity(BaseDraggingActivity activity, int widgetId, int requestCode)310 public void startConfigActivity(BaseDraggingActivity activity, int widgetId, int requestCode) { 311 if (WidgetsModel.GO_DISABLE_WIDGETS) { 312 sendActionCancelled(activity, requestCode); 313 return; 314 } 315 316 try { 317 TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startConfigActivity"); 318 startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode, 319 getConfigurationActivityOptions(activity, widgetId)); 320 } catch (ActivityNotFoundException | SecurityException e) { 321 Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); 322 sendActionCancelled(activity, requestCode); 323 } 324 } 325 326 /** 327 * Returns an {@link android.app.ActivityOptions} bundle from the {code activity} for launching 328 * the configuration of the {@code widgetId} app widget, or null of options cannot be produced. 329 */ 330 @Nullable getConfigurationActivityOptions(BaseDraggingActivity activity, int widgetId)331 private Bundle getConfigurationActivityOptions(BaseDraggingActivity activity, int widgetId) { 332 LauncherAppWidgetHostView view = mViews.get(widgetId); 333 if (view == null) return null; 334 Object tag = view.getTag(); 335 if (!(tag instanceof ItemInfo)) return null; 336 Bundle bundle = activity.getActivityLaunchOptions(view, (ItemInfo) tag).toBundle(); 337 bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY); 338 return bundle; 339 } 340 sendActionCancelled(final BaseActivity activity, final int requestCode)341 private void sendActionCancelled(final BaseActivity activity, final int requestCode) { 342 new Handler().post(() -> activity.onActivityResult(requestCode, RESULT_CANCELED, null)); 343 } 344 345 /** 346 * Listener for getting notifications on provider changes. 347 */ 348 public interface ProviderChangedListener { 349 notifyWidgetProvidersChanged()350 void notifyWidgetProvidersChanged(); 351 } 352 } 353