aboutsummaryrefslogtreecommitdiff
path: root/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppWidgetManager.java
blob: b1d78d1f4bcf0858dbdb8305b4dc40bc0d668101 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.L;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.O;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.annotation.Nullable;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build.VERSION;
import android.os.Bundle;
import android.os.UserHandle;
import android.view.View;
import android.widget.RemoteViews;
import com.android.internal.appwidget.IAppWidgetService;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.ForType;

@SuppressWarnings({"UnusedDeclaration"})
@Implements(AppWidgetManager.class)
public class ShadowAppWidgetManager {

  @RealObject private AppWidgetManager realAppWidgetManager;

  private Context context;
  private final Map<Integer, WidgetInfo> widgetInfos = new HashMap<>();
  private int nextWidgetId = 1;
  private boolean alwaysRecreateViewsDuringUpdate = false;
  private boolean allowedToBindWidgets;
  private boolean requestPinAppWidgetSupported = false;
  private boolean validWidgetProviderComponentName = true;
  private final ArrayList<AppWidgetProviderInfo> installedProviders = new ArrayList<>();
  private Multimap<UserHandle, AppWidgetProviderInfo> installedProvidersForProfile =
      HashMultimap.create();

  // AppWidgetProvider is enabled if at least one widget is active. `isWidgetsEnabled` should be set
  //  to false if the last widget is removed (when removing widgets is implemented).
  private boolean isWidgetsEnabled = false;

  @Implementation(maxSdk = KITKAT)
  protected void __constructor__(Context context) {
    this.context = context;
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void __constructor__(Context context, IAppWidgetService service) {
    this.context = context;
  }

  @Implementation
  protected void updateAppWidget(int[] appWidgetIds, RemoteViews views) {
    for (int appWidgetId : appWidgetIds) {
      updateAppWidget(appWidgetId, views);
    }
  }

  /**
   * Simulates updating an {@code AppWidget} with a new set of views
   *
   * @param appWidgetId id of widget
   * @param views views to update
   */
  @Implementation
  protected void updateAppWidget(int appWidgetId, RemoteViews views) {
    WidgetInfo widgetInfo = widgetInfos.get(appWidgetId);
    if (canReapplyRemoteViews(widgetInfo, views)) {
      views.reapply(context, widgetInfo.view);
    } else {
      widgetInfo.view = views.apply(context, new AppWidgetHostView(context));
      widgetInfo.layoutId = getRemoteViewsToApply(views).getLayoutId();
    }
    widgetInfo.lastRemoteViews = views;
  }

  private boolean canReapplyRemoteViews(WidgetInfo widgetInfo, RemoteViews views) {
    if (alwaysRecreateViewsDuringUpdate) {
      return false;
    }
    if (VERSION.SDK_INT < 25 && !hasLandscapeAndPortraitLayouts(views)) {
      return widgetInfo.layoutId == views.getLayoutId();
    }
    RemoteViews remoteViewsToApply = getRemoteViewsToApply(views);
    if (VERSION.SDK_INT >= 25) {
      return widgetInfo.layoutId == remoteViewsToApply.getLayoutId();
    } else {
      return widgetInfo.view != null && widgetInfo.view.getId() == remoteViewsToApply.getLayoutId();
    }
  }

  private RemoteViews getRemoteViewsToApply(RemoteViews views) {
    return reflector(RemoteViewsReflector.class, views).getRemoteViewsToApply(context);
  }

  private static boolean hasLandscapeAndPortraitLayouts(RemoteViews views) {
    return reflector(RemoteViewsReflector.class, views).hasLandscapeAndPortraitLayouts();
  }

  @Implementation
  protected int[] getAppWidgetIds(ComponentName provider) {
    List<Integer> idList = new ArrayList<>();
    for (int id : widgetInfos.keySet()) {
      WidgetInfo widgetInfo = widgetInfos.get(id);
      if (provider.equals(widgetInfo.providerComponent)) {
        idList.add(id);
      }
    }
    int ids[] = new int[idList.size()];
    for (int i = 0; i < idList.size(); i++) {
      ids[i] = idList.get(i);
    }
    return ids;
  }

  @Implementation
  protected List<AppWidgetProviderInfo> getInstalledProviders() {
    return new ArrayList<>(installedProviders);
  }

  @Implementation(minSdk = L)
  protected List<AppWidgetProviderInfo> getInstalledProvidersForProfile(UserHandle profile) {
    return ImmutableList.copyOf(installedProvidersForProfile.get(profile));
  }

  @Implementation(minSdk = O)
  protected List<AppWidgetProviderInfo> getInstalledProvidersForPackage(
      String packageName, UserHandle profile) {
    return ImmutableList.copyOf(
        installedProvidersForProfile.get(profile).stream()
            .filter(
                (AppWidgetProviderInfo providerInfo) ->
                    providerInfo.provider.getPackageName().equals(packageName))
            .collect(Collectors.toList()));
  }

  public void addInstalledProvider(AppWidgetProviderInfo appWidgetProviderInfo) {
    installedProviders.add(appWidgetProviderInfo);
  }

  public boolean removeInstalledProvider(AppWidgetProviderInfo appWidgetProviderInfo) {
    return installedProviders.remove(appWidgetProviderInfo);
  }

  public void addInstalledProvidersForProfile(
      UserHandle userHandle, AppWidgetProviderInfo appWidgetProviderInfo) {
    installedProvidersForProfile.put(userHandle, appWidgetProviderInfo);
  }

  public void addBoundWidget(int appWidgetId, AppWidgetProviderInfo providerInfo) {
    addInstalledProvider(providerInfo);
    bindAppWidgetId(appWidgetId, providerInfo.provider);
    widgetInfos.get(appWidgetId).info = providerInfo;
  }

  @Deprecated
  public void putWidgetInfo(int appWidgetId, AppWidgetProviderInfo expectedWidgetInfo) {
    addBoundWidget(appWidgetId, expectedWidgetInfo);
  }

  @Implementation
  protected AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) {
    WidgetInfo widgetInfo = widgetInfos.get(appWidgetId);
    if (widgetInfo == null) return null;
    return widgetInfo.info;
  }

  /** Gets the appWidgetOptions Bundle stored in a local cache. */
  @Implementation
  protected Bundle getAppWidgetOptions(int appWidgetId) {
    WidgetInfo widgetInfo = widgetInfos.get(appWidgetId);
    if (widgetInfo == null) {
      return Bundle.EMPTY;
    }
    return (Bundle) widgetInfo.options.clone();
  }

  /**
   * Update the locally cached appWidgetOptions Bundle. Instead of triggering associated
   * AppWidgetProvider.onAppWidgetOptionsChanged through Intent, this implementation calls the
   * method directly.
   */
  @Implementation
  protected void updateAppWidgetOptions(int appWidgetId, Bundle options) {
    WidgetInfo widgetInfo = widgetInfos.get(appWidgetId);
    if (widgetInfo != null && options != null) {
      widgetInfo.options.putAll(options);
      if (widgetInfo.appWidgetProvider != null) {
        widgetInfo.appWidgetProvider.onAppWidgetOptionsChanged(
            context, realAppWidgetManager, appWidgetId, (Bundle) options.clone());
      }
    }
  }

  @HiddenApi
  @Implementation
  public void bindAppWidgetId(int appWidgetId, ComponentName provider) {
    bindAppWidgetId(appWidgetId, provider, null);
  }

  @HiddenApi
  @Implementation
  protected void bindAppWidgetId(int appWidgetId, ComponentName provider, Bundle options) {
    WidgetInfo widgetInfo = new WidgetInfo(provider);
    widgetInfos.put(appWidgetId, widgetInfo);
    if (options != null) {
      widgetInfo.options = (Bundle) options.clone();
    }
    for (AppWidgetProviderInfo appWidgetProviderInfo : installedProviders) {
      if (provider != null && provider.equals(appWidgetProviderInfo.provider)) {
        widgetInfo.info = appWidgetProviderInfo;
      }
    }
  }

  /**
   * Create an internal presentation of the widget and cache it locally. This implementation doesn't
   * trigger {@code AppWidgetProvider.onUpdate}
   */
  @Implementation
  protected boolean bindAppWidgetIdIfAllowed(int appWidgetId, ComponentName provider) {
    return bindAppWidgetIdIfAllowed(appWidgetId, provider, null);
  }

  /**
   * Create an internal presentation of the widget locally and store the options {@link Bundle} with
   * it. This implementation doesn't trigger {@code AppWidgetProvider.onUpdate}
   */
  @Implementation
  protected boolean bindAppWidgetIdIfAllowed(
      int appWidgetId, ComponentName provider, Bundle options) {
    if (validWidgetProviderComponentName) {
      bindAppWidgetId(appWidgetId, provider, options);
      return allowedToBindWidgets;
    } else {
      throw new IllegalArgumentException("not an appwidget provider");
    }
  }

  /** Returns true if {@link setSupportedToRequestPinAppWidget} is called with {@code true} */
  @Implementation(minSdk = O)
  protected boolean isRequestPinAppWidgetSupported() {
    return requestPinAppWidgetSupported;
  }

  /**
   * This implementation currently uses {@code requestPinAppWidgetSupported} to determine if it
   * should bind the app widget provided and execute the {@code successCallback}.
   *
   * <p>Note: This implementation doesn't trigger {@code AppWidgetProvider.onUpdate}.
   *
   * @param provider The provider for the app widget to bind.
   * @param extras Returned in the callback along with the ID of the newly bound app widget, sent as
   *     {@link AppWidgetManager#EXTRA_APPWIDGET_ID}.
   * @param successCallback Called after binding the app widget, if possible.
   * @return true if the widget was installed, false otherwise.
   */
  @Implementation(minSdk = O)
  protected boolean requestPinAppWidget(
      ComponentName provider, @Nullable Bundle extras, @Nullable PendingIntent successCallback) {
    if (requestPinAppWidgetSupported) {
      int myWidgetId = nextWidgetId++;
      // Bind the widget.
      bindAppWidgetId(myWidgetId, provider);

      // Call the success callback if it exists.
      if (successCallback != null) {
        try {
          successCallback.send(
              context.getApplicationContext(),
              0,
              new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, myWidgetId));
        } catch (CanceledException e) {
          throw new RuntimeException(e);
        }
      }
      return true;
    }

    return false;
  }

  /**
   * Triggers a reapplication of the most recent set of actions against the widget, which is what
   * happens when the phone is rotated. Does not attempt to simulate a change in screen geometry.
   *
   * @param appWidgetId the ID of the widget to be affected
   */
  public void reconstructWidgetViewAsIfPhoneWasRotated(int appWidgetId) {
    WidgetInfo widgetInfo = widgetInfos.get(appWidgetId);
    widgetInfo.view = widgetInfo.lastRemoteViews.apply(context, new AppWidgetHostView(context));
  }

  private void enableWidgetsIfNecessary(Class<? extends AppWidgetProvider> appWidgetProviderClass) {
    if (!isWidgetsEnabled) {
      isWidgetsEnabled = true;
      AppWidgetProvider appWidgetProvider =
          ReflectionHelpers.callConstructor(appWidgetProviderClass);
      appWidgetProvider.onReceive(context, new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLED));
    }
  }

  /**
   * Creates a widget by inflating its layout.
   *
   * @param appWidgetProviderClass the app widget provider class
   * @param widgetLayoutId id of the layout to inflate
   * @return the ID of the new widget
   */
  public int createWidget(
      Class<? extends AppWidgetProvider> appWidgetProviderClass, int widgetLayoutId) {
    return createWidgets(appWidgetProviderClass, widgetLayoutId, 1)[0];
  }

  /**
   * Creates a bunch of widgets by inflating the same layout multiple times.
   *
   * @param appWidgetProviderClass the app widget provider class
   * @param widgetLayoutId id of the layout to inflate
   * @param howManyToCreate number of new widgets to create
   * @return the IDs of the new widgets
   */
  public int[] createWidgets(
      Class<? extends AppWidgetProvider> appWidgetProviderClass,
      int widgetLayoutId,
      int howManyToCreate) {
    AppWidgetProvider appWidgetProvider = ReflectionHelpers.callConstructor(appWidgetProviderClass);

    int[] newWidgetIds = new int[howManyToCreate];
    for (int i = 0; i < howManyToCreate; i++) {
      int myWidgetId = nextWidgetId++;
      RemoteViews remoteViews = new RemoteViews(context.getPackageName(), widgetLayoutId);
      View widgetView = remoteViews.apply(context, new AppWidgetHostView(context));
      WidgetInfo widgetInfo =
          new WidgetInfo(widgetView, widgetLayoutId, context, appWidgetProvider);
      widgetInfo.lastRemoteViews = remoteViews;
      widgetInfos.put(myWidgetId, widgetInfo);
      newWidgetIds[i] = myWidgetId;
    }

    // Enable widgets if we are creating the first widget.
    enableWidgetsIfNecessary(appWidgetProviderClass);

    Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, newWidgetIds);
    appWidgetProvider.onReceive(context, intent);
    return newWidgetIds;
  }

  /**
   * @param widgetId id of the desired widget
   * @return the widget associated with {@code widgetId}
   */
  public View getViewFor(int widgetId) {
    return widgetInfos.get(widgetId).view;
  }

  /**
   * @param widgetId id of the widget whose provider is to be returned
   * @return the {@code AppWidgetProvider} associated with {@code widgetId}
   */
  public AppWidgetProvider getAppWidgetProviderFor(int widgetId) {
    return widgetInfos.get(widgetId).appWidgetProvider;
  }

  /**
   * Enables testing of widget behavior when all of the views are recreated on every update. This is
   * useful for ensuring that your widget will behave correctly even if it is restarted by the OS
   * between events.
   *
   * @param alwaysRecreate whether or not to always recreate the views
   */
  public void setAlwaysRecreateViewsDuringUpdate(boolean alwaysRecreate) {
    alwaysRecreateViewsDuringUpdate = alwaysRecreate;
  }

  /**
   * @return the state of the{@code alwaysRecreateViewsDuringUpdate} flag
   */
  public boolean getAlwaysRecreateViewsDuringUpdate() {
    return alwaysRecreateViewsDuringUpdate;
  }

  public void setAllowedToBindAppWidgets(boolean allowed) {
    allowedToBindWidgets = allowed;
  }

  public void setRequestPinAppWidgetSupported(boolean supported) {
    requestPinAppWidgetSupported = supported;
  }

  public void setValidWidgetProviderComponentName(boolean validWidgetProviderComponentName) {
    this.validWidgetProviderComponentName = validWidgetProviderComponentName;
  }

  private static class WidgetInfo {
    View view;
    int layoutId;
    final AppWidgetProvider appWidgetProvider;
    RemoteViews lastRemoteViews;
    final ComponentName providerComponent;
    AppWidgetProviderInfo info;
    Bundle options = new Bundle();

    public WidgetInfo(
        View view, int layoutId, Context context, AppWidgetProvider appWidgetProvider) {
      this.view = view;
      this.layoutId = layoutId;
      this.appWidgetProvider = appWidgetProvider;
      providerComponent = new ComponentName(context, appWidgetProvider.getClass());
    }

    public WidgetInfo(ComponentName providerComponent) {
      this.providerComponent = providerComponent;
      this.appWidgetProvider = null;
    }
  }

  @ForType(RemoteViews.class)
  interface RemoteViewsReflector {
    RemoteViews getRemoteViewsToApply(Context context);

    boolean hasLandscapeAndPortraitLayouts();
  }
}