blob: 300200ba9465055234e72f6f320d41ef449568db [file] [log] [blame]
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
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 android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.os.UserHandle;
import android.view.LayoutInflater;
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.RuntimeEnvironment;
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;
@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();
@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);
int layoutId = views.getLayoutId();
if (widgetInfo.layoutId != layoutId || alwaysRecreateViewsDuringUpdate) {
widgetInfo.view = createWidgetView(layoutId);
widgetInfo.layoutId = layoutId;
}
widgetInfo.lastRemoteViews = views;
views.reapply(context, widgetInfo.view);
}
@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 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(minSdk = JELLY_BEAN_MR1)
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(minSdk = JELLY_BEAN_MR1)
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;
}
/**
* 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 = createWidgetView(widgetInfo.layoutId);
widgetInfo.lastRemoteViews.reapply(context, widgetInfo.view);
}
/**
* 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++) {
View widgetView = createWidgetView(widgetLayoutId);
int myWidgetId = nextWidgetId++;
widgetInfos.put(
myWidgetId, new WidgetInfo(widgetView, widgetLayoutId, context, appWidgetProvider));
newWidgetIds[i] = myWidgetId;
}
appWidgetProvider.onUpdate(context, realAppWidgetManager, newWidgetIds);
return newWidgetIds;
}
private View createWidgetView(int widgetLayoutId) {
return LayoutInflater.from(RuntimeEnvironment.getApplication()).inflate(widgetLayoutId, null);
}
/**
* @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;
}
}
}