I have a widget that has a ListView of items. When the widget is new and empty, user clicks on it to load an activity with list of objects (each object contain the list of items) to select an object and then the widget should receive it and update its content to display it. I managed to reach the stage where the object (containing the list) is received by the AppWidgetProvider and update is called. What I'm failing to do is to make the provider call the RemoteViewService and further steps. I'll include the classes and XMLs for review.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.litebit.ilfornodellacasa">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".ui.activities.MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ui.activities.RecipeActivity"
android:launchMode="singleTop">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.activities.MainActivity"/>
</activity>
<receiver android:name=".ui.widgets.IngredientsWidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/ingredients_app_widget_info"/>
</receiver>
<service
android:name=".ui.widgets.WidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS"/>
</application>
</manifest>
IngredientsWidgetProvider.java
package io.litebit.ilfornodellacasa.ui.widgets;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import io.litebit.ilfornodellacasa.R;
import io.litebit.ilfornodellacasa.model.Recipe;
import io.litebit.ilfornodellacasa.ui.activities.MainActivity;
import io.litebit.ilfornodellacasa.ui.activities.RecipeActivity;
import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
/**
* Implementation of App Widget functionality.
*/
public class IngredientsWidgetProvider extends AppWidgetProvider {
private static final String TAG = IngredientsWidgetProvider.class.getSimpleName();
private static Recipe recipe;
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
// Construct the RemoteViews object
RemoteViews widget = new RemoteViews(context.getPackageName(),
R.layout.ingredients_app_widget);
// Create pending intent to open the MainActivity
Intent mainActivityIntent = new Intent(context, MainActivity.class);
mainActivityIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId);
mainActivityIntent.setAction(MainActivity.ACTION_UPDATE_WIDGET);
PendingIntent pendingIntent = PendingIntent.getActivity(context,
0, mainActivityIntent, 0);
// Launch pending intent on click
widget.setOnClickPendingIntent(R.id.widget_layout, pendingIntent);
if (recipe != null) {
Log.i(TAG, "Recipe: " + recipe.getName() + " to be visualized");
widget.setViewVisibility(R.id.tv_widget_empty, View.GONE);
widget.setViewVisibility(R.id.lv_widget, View.VISIBLE);
Intent listIntent = new Intent(context, WidgetService.class);
listIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
listIntent.putExtra(RecipeActivity.KEY_RECIPE, recipe);
Uri uri = Uri.parse(listIntent.toUri(Intent.URI_INTENT_SCHEME));
listIntent.setData(uri);
widget.setRemoteAdapter(R.id.lv_widget, listIntent);
widget.setEmptyView(R.id.lv_widget, R.id.tv_widget_empty);
} else {
widget.setViewVisibility(R.id.tv_widget_empty, View.VISIBLE);
widget.setViewVisibility(R.id.lv_widget, View.GONE);
}
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, widget);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
@Override
public void onEnabled(Context context) {
// Enter relevant functionality for when the first widget is created
}
@Override
public void onDisabled(Context context) {
// Enter relevant functionality for when the last widget is disabled
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
recipe = intent.getParcelableExtra(RecipeActivity.KEY_RECIPE);
if (recipe != null) {
Log.i(TAG, "Recipe: " + recipe.getName() + " selected");
updateAppWidget(context, AppWidgetManager.getInstance(context),
intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID));
}
}
}
IngredientViewHolderFactory.java
package io.litebit.ilfornodellacasa.ui.widgets;
import android.content.Context;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
import java.util.ArrayList;
import java.util.List;
import io.litebit.ilfornodellacasa.R;
import io.litebit.ilfornodellacasa.model.Ingredient;
import io.litebit.ilfornodellacasa.ui.utils.Utils;
/**
* Copyright 2017 Ramy Bittar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
public class IngredientViewHolderFactory implements RemoteViewsService.RemoteViewsFactory {
private static final String TAG = IngredientViewHolderFactory.class.getSimpleName();
private List<Ingredient> ingredients = new ArrayList<>();
private Context context;
private int appWidgetId;
private Utils utils;
IngredientViewHolderFactory(Context context, List<Ingredient> ingredients, int appWidgetId) {
this.context = context;
this.appWidgetId = appWidgetId;
this.ingredients = ingredients;
utils = new Utils(context);
Log.i(TAG, "Public constructor");
}
@Override
public void onCreate() {
Log.i(TAG, "appWidgetId = " + this.appWidgetId);
}
@Override
public void onDataSetChanged() {
}
@Override
public void onDestroy() {
}
@Override
public int getCount() {
if (ingredients != null) {
return ingredients.size();
}
return 0;
}
@Override
public RemoteViews getViewAt(int i) {
final RemoteViews viewHolder = new RemoteViews(context.getPackageName(),
R.layout.viewholder_ingredient);
Ingredient ingredient = ingredients.get(i);
viewHolder.setTextViewText(R.id.tv_ingredient, ingredient.getIngredient());
String quantity = utils.getQuantity(
ingredient.getQuantity(),
ingredient.getMeasure(),
Utils.UNIT_SYS_IMPERIAL);
viewHolder.setTextViewText(R.id.tv_quantity, quantity);
return viewHolder;
}
@Override
public RemoteViews getLoadingView() {
return null;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return false;
}
}
WidgetService.java
package io.litebit.ilfornodellacasa.ui.widgets;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.util.Log;
import android.widget.RemoteViewsService;
import io.litebit.ilfornodellacasa.model.Recipe;
import io.litebit.ilfornodellacasa.ui.activities.RecipeActivity;
/**
* Copyright 2017 Ramy Bittar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
public class WidgetService extends RemoteViewsService {
private static final String TAG = WidgetService.class.getSimpleName();
/**
* Invokes the remote view factory
* @param intent passed from the calling widget provider to the remote view factory
* @return RemoteViewsFactory object
*/
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
Log.i(TAG, "onGetViewFactory called");
Recipe recipe = intent.getParcelableExtra(RecipeActivity.KEY_RECIPE);
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
return (new IngredientViewHolderFactory(this.getApplicationContext(),
recipe.getIngredients(),
appWidgetId));
}
}
MainActivity.java
package io.litebit.ilfornodellacasa.ui.activities;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import java.util.List;
import io.litebit.ilfornodellacasa.R;
import io.litebit.ilfornodellacasa.model.Recipe;
import io.litebit.ilfornodellacasa.model.RecipeListSerializer;
import io.litebit.ilfornodellacasa.sync.RecipeSyncTask;
import io.litebit.ilfornodellacasa.ui.adapters.RecipeAdapter;
import io.litebit.ilfornodellacasa.ui.widgets.IngredientsWidgetProvider;
import pocketknife.BundleSerializer;
import pocketknife.PocketKnife;
import pocketknife.SaveState;
/**
* Copyright 2017 Ramy Bittar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
public class MainActivity extends AppCompatActivity implements RecipeSyncTask.SyncRecipesCallback,
RecipeAdapter.OnRecipeClicked {
// private static final String TAG = MainActivity.class.getSimpleName();
private static final String TAG = MainActivity.class.getSimpleName();
public static final String ACTION_UPDATE_WIDGET = TAG + ".action.update_widget";
private boolean updateWidget = false;
private int appWidgetId;
private RecipeAdapter adapter;
@SaveState
@BundleSerializer(RecipeListSerializer.class)
List<Recipe> recipes;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PocketKnife.bindExtras(this);
PocketKnife.restoreInstanceState(this, savedInstanceState);
RecyclerView recyclerView = findViewById(R.id.recycler_view);
RecyclerView.LayoutManager layoutManager;
if (isTabletAndLandscape()) {
layoutManager = new GridLayoutManager(this, 3);
} else {
layoutManager = new LinearLayoutManager(this);
}
recyclerView.setLayoutManager(layoutManager);
adapter = new RecipeAdapter(null, this);
recyclerView.setAdapter(adapter);
if (recipes == null || recipes.size() == 0) {
RecipeSyncTask syncTask = new RecipeSyncTask(this);
syncTask.syncRecipes();
} else {
refreshRecyclerView(recipes);
}
if (!getIntent().getAction().equals("")) {
updateWidget = getIntent().getAction().equals(ACTION_UPDATE_WIDGET);
appWidgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0);
}
Log.i(TAG, "updateWidget = " + updateWidget);
}
private boolean isTabletAndLandscape() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE
&& getResources().getConfiguration().screenWidthDp >= 900;
}
private void refreshRecyclerView(List<Recipe> recipes) {
this.adapter.switchData(recipes);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
PocketKnife.saveInstanceState(this, outState);
super.onSaveInstanceState(outState);
}
@Override
public void onSyncResponse(List<Recipe> newRecipes) {
recipes = newRecipes;
refreshRecyclerView(this.recipes);
Toast.makeText(this, recipes.size() + " recipe(s) found.", Toast.LENGTH_SHORT).show();
}
@Override
public void onSyncFailure(Throwable throwable) {
Toast.makeText(this, "Something wrong happened. Check system log for details.",
Toast.LENGTH_SHORT).show();
}
@Override
public void onClick(int recipeId) {
Recipe currentRecipe = null;
for (Recipe recipe : recipes) {
if (recipe.getId() == recipeId) {
currentRecipe = recipe;
}
}
if (updateWidget) {
sendIntentToWidget(currentRecipe);
} else {
sendIntentToRecipeActivity(currentRecipe);
}
}
private void sendIntentToWidget(Recipe currentRecipe) {
Intent recipeIntent = new Intent(this, IngredientsWidgetProvider.class);
recipeIntent.putExtra(RecipeActivity.KEY_RECIPE, currentRecipe);
recipeIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
recipeIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
sendBroadcast(recipeIntent);
finish();
}
private void sendIntentToRecipeActivity(Recipe currentRecipe) {
Intent recipeIntent = new Intent(this, RecipeActivity.class);
recipeIntent.putExtra(RecipeActivity.KEY_RECIPE, currentRecipe);
recipeIntent.putExtra(RecipeActivity.KEY_STEP_ID, RecipeActivity.NO_STEP_SELECTED);
startActivity(recipeIntent);
}
}
viewholder_ingredient.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="0dp"
android:paddingLeft="14dp"
android:paddingRight="14dp">
<TextView
android:id="@+id/tv_ingredient"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"/>
<TextView
android:id="@+id/tv_quantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
ingredients_app_widget.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#66ffffff"
android:orientation="vertical"
android:padding="@dimen/widget_margin">
<ListView
android:id="@+id/lv_widget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"/>
<TextView
android:id="@+id/tv_widget_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/click_to_select_a_recipe"
android:visibility="gone"
tools:text="Empty list text"/>
</LinearLayout>
ingredients_app_widget_info.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:initialKeyguardLayout="@layout/ingredients_app_widget"
android:initialLayout="@layout/ingredients_app_widget"
android:minHeight="125dp"
android:minWidth="250dp"
android:previewImage="@mipmap/ic_launcher"
android:resizeMode="vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"
android:configure="io.litebit.ilfornodellacasa.ui.activities.MainActivity"
tools:targetApi="jelly_bean_mr1">
</appwidget-provider>
Thanks for help in advance.
Ramy Bittar
It seems to me that you are not calling
notifyAppWidgetViewDataChanged
. If the data has changed you need to notify the widget that the collection view needs to be updated.AppWidgetManager.notifyAppWidgetViewDataChanged
Also, take a look in the collections sample of the widgets the topic Keeping Collection Data Fresh