Weather List Widget
//
//src\com\example\android\weatherlistwidget\WeatherDataProvider.java
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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.
*/
package com.example.android.weatherlistwidget;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import java.util.ArrayList;
/**
* A dummy class that we are going to use internally to store weather data. Generally, this data
* will be stored in an external and persistent location (ie. File, Database, SharedPreferences) so
* that the data can persist if the process is ever killed. For simplicity, in this sample the
* data will only be stored in memory.
*/
class WeatherDataPoint {
String city;
int degrees;
WeatherDataPoint(String c, int d) {
city = c;
degrees = d;
}
}
/**
* The AppWidgetProvider for our sample weather widget.
*/
public class WeatherDataProvider extends ContentProvider {
public static final Uri CONTENT_URI =
Uri.parse("content://com.example.android.weatherlistwidget.provider");
public static class Columns {
public static final String ID = "_id";
public static final String CITY = "city";
public static final String TEMPERATURE = "temperature";
}
/**
* Generally, this data will be stored in an external and persistent location (ie. File,
* Database, SharedPreferences) so that the data can persist if the process is ever killed.
* For simplicity, in this sample the data will only be stored in memory.
*/
private static final ArrayList<WeatherDataPoint> sData = new ArrayList<WeatherDataPoint>();
@Override
public boolean onCreate() {
// We are going to initialize the data provider with some default values
sData.add(new WeatherDataPoint("San Francisco", 13));
sData.add(new WeatherDataPoint("New York", 1));
sData.add(new WeatherDataPoint("Seattle", 7));
sData.add(new WeatherDataPoint("Boston", 4));
sData.add(new WeatherDataPoint("Miami", 22));
sData.add(new WeatherDataPoint("Toronto", -10));
sData.add(new WeatherDataPoint("Calgary", -13));
sData.add(new WeatherDataPoint("Tokyo", 8));
sData.add(new WeatherDataPoint("Kyoto", 11));
sData.add(new WeatherDataPoint("London", -1));
sData.add(new WeatherDataPoint("Nomanisan", 27));
return true;
}
@Override
public synchronized Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
assert(uri.getPathSegments().isEmpty());
// In this sample, we only query without any parameters, so we can just return a cursor to
// all the weather data.
final MatrixCursor c = new MatrixCursor(
new String[]{ Columns.ID, Columns.CITY, Columns.TEMPERATURE });
for (int i = 0; i < sData.size(); ++i) {
final WeatherDataPoint data = sData.get(i);
c.addRow(new Object[]{ new Integer(i), data.city, new Integer(data.degrees) });
}
return c;
}
@Override
public String getType(Uri uri) {
return "vnd.android.cursor.dir/vnd.weatherlistwidget.citytemperature";
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// This example code does not support inserting
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// This example code does not support deleting
return 0;
}
@Override
public synchronized int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
assert(uri.getPathSegments().size() == 1);
// In this sample, we only update the content provider individually for each row with new
// temperature values.
final int index = Integer.parseInt(uri.getPathSegments().get(0));
final MatrixCursor c = new MatrixCursor(
new String[]{ Columns.ID, Columns.CITY, Columns.TEMPERATURE });
assert(0 <= index && index < sData.size());
final WeatherDataPoint data = sData.get(index);
data.degrees = values.getAsInteger(Columns.TEMPERATURE);
// Notify any listeners that the data backing the content provider has changed, and return
// the number of rows affected.
getContext().getContentResolver().notifyChange(uri, null);
return 1;
}
}
//src\com\example\android\weatherlistwidget\WeatherWidgetProvider.java
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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.
*/
package com.example.android.weatherlistwidget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.database.Cursor;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.widget.RemoteViews;
import android.widget.Toast;
import java.util.Random;
/**
* Our data observer just notifies an update for all weather widgets when it detects a change.
*/
class WeatherDataProviderObserver extends ContentObserver {
private AppWidgetManager mAppWidgetManager;
private ComponentName mComponentName;
WeatherDataProviderObserver(AppWidgetManager mgr, ComponentName cn, Handler h) {
super(h);
mAppWidgetManager = mgr;
mComponentName = cn;
}
@Override
public void onChange(boolean selfChange) {
// The data has changed, so notify the widget that the collection view needs to be updated.
// In response, the factory's onDataSetChanged() will be called which will requery the
// cursor for the new data.
mAppWidgetManager.notifyAppWidgetViewDataChanged(
mAppWidgetManager.getAppWidgetIds(mComponentName), R.id.weather_list);
}
}
/**
* The weather widget's AppWidgetProvider.
*/
public class WeatherWidgetProvider extends AppWidgetProvider {
public static String CLICK_ACTION = "com.example.android.weatherlistwidget.CLICK";
public static String REFRESH_ACTION = "com.example.android.weatherlistwidget.REFRESH";
public static String EXTRA_CITY_ID = "com.example.android.weatherlistwidget.city";
private static HandlerThread sWorkerThread;
private static Handler sWorkerQueue;
private static WeatherDataProviderObserver sDataObserver;
public WeatherWidgetProvider() {
// Start the worker thread
sWorkerThread = new HandlerThread("WeatherWidgetProvider-worker");
sWorkerThread.start();
sWorkerQueue = new Handler(sWorkerThread.getLooper());
}
@Override
public void onEnabled(Context context) {
// Register for external updates to the data to trigger an update of the widget. When using
// content providers, the data is often updated via a background service, or in response to
// user interaction in the main app. To ensure that the widget always reflects the current
// state of the data, we must listen for changes and update ourselves accordingly.
final ContentResolver r = context.getContentResolver();
if (sDataObserver == null) {
final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
final ComponentName cn = new ComponentName(context, WeatherWidgetProvider.class);
sDataObserver = new WeatherDataProviderObserver(mgr, cn, sWorkerQueue);
r.registerContentObserver(WeatherDataProvider.CONTENT_URI, true, sDataObserver);
}
}
@Override
public void onReceive(Context ctx, Intent intent) {
final String action = intent.getAction();
if (action.equals(REFRESH_ACTION)) {
// BroadcastReceivers have a limited amount of time to do work, so for this sample, we
// are triggering an update of the data on another thread. In practice, this update
// can be triggered from a background service, or perhaps as a result of user actions
// inside the main application.
final Context context = ctx;
sWorkerQueue.removeMessages(0);
sWorkerQueue.post(new Runnable() {
@Override
public void run() {
final ContentResolver r = context.getContentResolver();
final Cursor c = r.query(WeatherDataProvider.CONTENT_URI, null, null, null,
null);
final int count = c.getCount();
final int maxDegrees = 96;
// We disable the data changed observer temporarily since each of the updates
// will trigger an onChange() in our data observer.
r.unregisterContentObserver(sDataObserver);
for (int i = 0; i < count; ++i) {
final Uri uri = ContentUris.withAppendedId(WeatherDataProvider.CONTENT_URI, i);
final ContentValues values = new ContentValues();
values.put(WeatherDataProvider.Columns.TEMPERATURE,
new Random().nextInt(maxDegrees));
r.update(uri, values, null, null);
}
r.registerContentObserver(WeatherDataProvider.CONTENT_URI, true, sDataObserver);
final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
final ComponentName cn = new ComponentName(context, WeatherWidgetProvider.class);
mgr.notifyAppWidgetViewDataChanged(mgr.getAppWidgetIds(cn), R.id.weather_list);
}
});
} else if (action.equals(CLICK_ACTION)) {
// Show a toast
final int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
final String city = intent.getStringExtra(EXTRA_CITY_ID);
final String formatStr = ctx.getResources().getString(R.string.toast_format_string);
Toast.makeText(ctx, String.format(formatStr, city), Toast.LENGTH_SHORT).show();
}
super.onReceive(ctx, intent);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// Update each of the widgets with the remote adapter
for (int i = 0; i < appWidgetIds.length; ++i) {
// Specify the service to provide data for the collection widget. Note that we need to
// embed the appWidgetId via the data otherwise it will be ignored.
final Intent intent = new Intent(context, WeatherWidgetService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
final RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
rv.setRemoteAdapter(appWidgetIds[i], R.id.weather_list, intent);
// Set the empty view to be displayed if the collection is empty. It must be a sibling
// view of the collection view.
rv.setEmptyView(R.id.weather_list, R.id.empty_view);
// Bind a click listener template for the contents of the weather list. Note that we
// need to update the intent's data if we set an extra, since the extras will be
// ignored otherwise.
final Intent onClickIntent = new Intent(context, WeatherWidgetProvider.class);
onClickIntent.setAction(WeatherWidgetProvider.CLICK_ACTION);
onClickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
onClickIntent.setData(Uri.parse(onClickIntent.toUri(Intent.URI_INTENT_SCHEME)));
final PendingIntent onClickPendingIntent = PendingIntent.getBroadcast(context, 0,
onClickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setPendingIntentTemplate(R.id.weather_list, onClickPendingIntent);
// Bind the click intent for the refresh button on the widget
final Intent refreshIntent = new Intent(context, WeatherWidgetProvider.class);
refreshIntent.setAction(WeatherWidgetProvider.REFRESH_ACTION);
final PendingIntent refreshPendingIntent = PendingIntent.getBroadcast(context, 0,
refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.refresh, refreshPendingIntent);
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
}
//src\com\example\android\weatherlistwidget\WeatherWidgetService.java
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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.
*/
package com.example.android.weatherlistwidget;
import java.util.ArrayList;
import java.util.List;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.content.ContentUris;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
/**
* This is the service that provides the factory to be bound to the collection service.
*/
public class WeatherWidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
/**
* This is the factory that will provide data to the collection widget.
*/
class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private Context mContext;
private Cursor mCursor;
private int mAppWidgetId;
public StackRemoteViewsFactory(Context context, Intent intent) {
mContext = context;
mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
public void onCreate() {
// Since we reload the cursor in onDataSetChanged() which gets called immediately after
// onCreate(), we do nothing here.
}
public void onDestroy() {
if (mCursor != null) {
mCursor.close();
}
}
public int getCount() {
return mCursor.getCount();
}
public RemoteViews getViewAt(int position) {
// Get the data for this position from the content provider
String city = "Unknown City";
int temp = 0;
if (mCursor.moveToPosition(position)) {
final int cityColIndex = mCursor.getColumnIndex(WeatherDataProvider.Columns.CITY);
final int tempColIndex = mCursor.getColumnIndex(
WeatherDataProvider.Columns.TEMPERATURE);
city = mCursor.getString(cityColIndex);
temp = mCursor.getInt(tempColIndex);
}
// Return a proper item with the proper city and temperature. Just for fun, we alternate
// the items to make the list easier to read.
final String formatStr = mContext.getResources().getString(R.string.item_format_string);
final int itemId = (position % 2 == 0 ? R.layout.light_widget_item
: R.layout.dark_widget_item);
RemoteViews rv = new RemoteViews(mContext.getPackageName(), itemId);
rv.setTextViewText(R.id.widget_item, String.format(formatStr, temp, city));
// Set the click intent so that we can handle it and show a toast message
final Intent fillInIntent = new Intent();
final Bundle extras = new Bundle();
extras.putString(WeatherWidgetProvider.EXTRA_CITY_ID, city);
fillInIntent.putExtras(extras);
rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);
return rv;
}
public RemoteViews getLoadingView() {
// We aren't going to return a default loading view in this sample
return null;
}
public int getViewTypeCount() {
// Technically, we have two types of views (the dark and light background views)
return 2;
}
public long getItemId(int position) {
return position;
}
public boolean hasStableIds() {
return true;
}
public void onDataSetChanged() {
// Refresh the cursor
if (mCursor != null) {
mCursor.close();
}
mCursor = mContext.getContentResolver().query(WeatherDataProvider.CONTENT_URI, null, null,
null, null);
}
}
//
//res\layout\dark_widget_item.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 The Android Open Source Project
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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_item"
android:layout_width="match_parent"
android:layout_height="46dp"
android:paddingLeft="25dp"
android:gravity="center_vertical"
android:background="@drawable/item_bg_dark"
android:textColor="#e5e5e1"
android:textSize="24sp" />
//res\layout\light_widget_item.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 The Android Open Source Project
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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_item"
android:layout_width="match_parent"
android:layout_height="46dp"
android:paddingLeft="25dp"
android:gravity="center_vertical"
android:background="@drawable/item_bg_light"
android:textColor="#e5e5e1"
android:textSize="24sp" />
//res\layout\widget_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 The Android Open Source Project
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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="294dp"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/header" />
<ImageButton
android:id="@+id/refresh"
android:layout_width="56dp"
android:layout_height="39dp"
android:layout_marginLeft="222dp"
android:layout_marginTop="20dp"
android:background="@drawable/refresh_button" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_gravity="center"
android:background="@drawable/body">
<ListView
android:id="@+id/weather_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="gone"
android:textColor="#ffffff"
android:text="@string/empty_view_text"
android:textSize="20sp" />
</FrameLayout>
<ImageView
android:id="@+id/footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/footer" />
</LinearLayout>
//
//res\values\strings.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 The Android Open Source Project
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.
-->
<resources>
<string name="empty_view_text">No cities found...</string>
<string name="toast_format_string">%1$s says Hi!</string>
<string name="item_format_string">%1$d\u00B0 in %2$s</string>
</resources>
//
//res\xml\widgetinfo.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 The Android Open Source Project
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.
-->
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="222dip"
android:minHeight="222dip"
android:updatePeriodMillis="1800000"
android:initialLayout="@layout/widget_layout"
android:resizeMode="vertical"
android:previewImage="@drawable/preview">
</appwidget-provider>
Related examples in the same category