//
//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 sData = new ArrayList();
@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
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
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
android:layout_width="294dp"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/header" />
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" />
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_gravity="center"
android:background="@drawable/body">
android:id="@+id/weather_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
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" />
android:id="@+id/footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/footer" />
//
//res\values\strings.xml
No cities found...
%1$s says Hi!
%1$d\u00B0 in %2$s
//
//res\xml\widgetinfo.xml
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">