2016年3月1日 星期二

Make the widget show the update data from the content provider

Make the widget show the update data from the content provider


Cncept:
WidgetProvider’s onUpdate() method use to uppdate the view of the widget
WidgetProvider is a brodcastreciver.
it receive the brodcast from the system,than run onReceive,the onReceive run onUpdate().
These are run in the UI thread.
So,we should not run some heavy task(need long time) in this two method.
We can use Intent Service.top get the data


the data flow is
.


we only know how  to get the data after the WidgetProvider receive a brodcast message from thesystem.but!!! how to make the system to send a brodcast when the data change?


we use sync adapter in this app.
we can make,when the syncadapter performsync,it can send a data changed brodcast.
than the WidgetProvider receive the brodcast,perform onReceive->onUpdate()->Intent service get the data from content provider.


to start the intent service, to get the data


This is the whole concept:
now,start to code!!!!
step1 create a IntentService
override the onHandleIntent method


step2 mevoe the code in onUpdate in TodayWidgetProvider to onHandleIntent to WidgetIntentService.
problem:
how to get AppWidgetManager in a intentservice?


AppWidgetManager appWidgetManager=AppWidgetManager.getInstance(this);


how to get appWidgetIds in a intentservice?
int[] appWidgetIds=appWidgetManager.getAppWidgetIds(new ComponentName(this, TodayWidgetProvider.class));


step3 in onUpdate in TodayWidgetProvider,start the service.


public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
       super.onUpdate(context, appWidgetManager, appWidgetIds);
       Intent serviceIntent=new Intent(context,WidgetIntentService.class);
      context.startService(serviceIntent);
   }


step4 because we want the sysnc adapter send brodcast when has new data


basic concept :how to send brodcast?


Intent i = new Intent("com.hmkcode.android.USER_ACTION");
sendBroadcast(i);


we want to send “com.example.android.sunshine.app.ACTION_DATA_UPDATED"”


so,in the SunshineSyncAdapter,under the notifyWeather(),because here the new weather data is in the SQL database now,call a method updateWidgets();


updateWidgets():
in this method,we send the bordcast


private void updateWidgets() {
       Context context=getContext();


       Intent intent=new Intent(INTENT_ACTION);
       context.sendBroadcast(intent);
   }

however,we only want our app can receive the brodcast:


只不過 Share Intent 會自動幫我們撈出目前安裝的 App 中,有哪些 App 是能夠支援我們所要發送的請求的;而透過指定特定 App 發送資料,就得必須要先知道該 App 的 Package Name 為何,才有辦法在建立 Intent 的時候指派進去,系統才知道屆時直接呼叫起這個 App 來發送,另外,如此一來也能夠利用


our pacakage name is


com.example.android.sunshine.app

private void updateWidgets() {
       Context context=getContext();


       Intent intent=new Intent(INTENT_ACTION);
//getPackageName Return the name of this application's package.
       intent.setPackage(context.getPackageName());
       context.sendBroadcast(intent);
   }

now, only our app can receive the brodcast.

step5:register the TodayWidgetProvider with "com.example.android.sunshine.app.ACTION_DATA_UPDATED"


now the app send brodcast ,but TodayWidgetProvider  can’t receive it,we need t o register it first in Manifest.xml


<receiver
           android:name=".widget.TodayWidgetProvider"
           android:label="@string/title_widget_today">
           <intent-filter>
               <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
               <action android:name="com.example.android.sunshine.app.ACTION_DATA_UPDATED" />
           </intent-filter>


step5 override the onReceive in TodayWidgetProvider


first code onUpdate first,:


Intent serviceIntent = new Intent(context, WidgetIntentService.class);
       context.startService(serviceIntent);


explain :start the intent service to give the view to the widget.


how ever,the onUpdate  only call onw time, because in widget_info_today.xml ,we set  android:updatePeriodMillis="0", that mean the system will not send again the android.appwidget.action.APPWIDGET_UPDATE to make the onUpdate(),unless the first time the widget created.


brodcast with android.appwidget.action.APPWIDGET_UPDATE will call onReceive ."com.example.android.sunshine.app.ACTION_DATA_UPDATED" will call onReceive too.


now ,code the onReceive :

  @Override
   public void onReceive(Context context, Intent intent) {
       super.onReceive(context, intent);
       if(intent.getAction().equals(SunshineSyncAdapter.INTENT_ACTION)){
           Intent serviceIntent = new Intent(context, WidgetIntentService.class);
           context.startService(serviceIntent);
       }
       


   }

brodcast with com.example.android.sunshine.app.ACTION_DATA_UPDATED will call onReceive ."android.appwidget.action.APPWIDGET_UPDATE" will call onReceive too.,sho we need to ensure is android.appwidget.action.APPWIDGET_UPDATE


now,the intent service can call correctly,however,we don’t query the data in it,now do it:


/*
* Copyright (C) 2015 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.sunshine.app.widget;


import android.annotation.TargetApi;
import android.app.IntentService;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.widget.RemoteViews;


import com.example.android.sunshine.app.MainActivity;
import com.example.android.sunshine.app.R;
import com.example.android.sunshine.app.Utility;
import com.example.android.sunshine.app.data.WeatherContract;


/**
* IntentService which handles updating all Today widgets with the latest data
*/
public class WidgetIntentService extends IntentService {
   private static final String[] FORECAST_COLUMNS = {
           WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
           WeatherContract.WeatherEntry.COLUMN_SHORT_DESC,
           WeatherContract.WeatherEntry.COLUMN_MAX_TEMP
   };
   // these indices must match the projection
   private static final int INDEX_WEATHER_ID = 0;
   private static final int INDEX_SHORT_DESC = 1;
   private static final int INDEX_MAX_TEMP = 2;


   public WidgetIntentService() {
       super("TodayWidgetIntentService");
   }


   @Override
   protected void onHandleIntent(Intent intent) {
       // Retrieve all of the Today widget ids: these are the widgets we need to update
       AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
       int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this,
               TodayWidgetProvider.class));


       // Get today's data from the ContentProvider
       String location = Utility.getPreferredLocation(this);
       Uri weatherForLocationUri = WeatherContract.WeatherEntry.buildWeatherLocationWithStartDate(
               location, System.currentTimeMillis());
       Cursor data = getContentResolver().query(weatherForLocationUri, FORECAST_COLUMNS, null,
               null, WeatherContract.WeatherEntry.COLUMN_DATE + " ASC");
       if (data == null) {
           return;
       }
       if (!data.moveToFirst()) {
           data.close();
           return;
       }


       // Extract the weather data from the Cursor
       int weatherId = data.getInt(INDEX_WEATHER_ID);
       int weatherArtResourceId = Utility.getArtResourceForWeatherCondition(weatherId);
       String description = data.getString(INDEX_SHORT_DESC);
       double maxTemp = data.getDouble(INDEX_MAX_TEMP);
       String formattedMaxTemperature = Utility.formatTemperature(this, maxTemp);
       data.close();


       // Perform this loop procedure for each Today widget
       for (int appWidgetId : appWidgetIds) {
           int layoutId = R.layout.widget_today_small;
           RemoteViews views = new RemoteViews(getPackageName(), layoutId);


           // Add the data to the RemoteViews
           views.setImageViewResource(R.id.widget_icon, weatherArtResourceId);
           // Content Descriptions for RemoteViews were only added in ICS MR1
           if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
               setRemoteContentDescription(views, description);
           }
           views.setTextViewText(R.id.widget_high_temperature, formattedMaxTemperature);


           // Create an Intent to launch MainActivity
           Intent launchIntent = new Intent(this, MainActivity.class);
           PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, 0);
           views.setOnClickPendingIntent(R.id.widget, pendingIntent);


           // Tell the AppWidgetManager to perform an update on the current app widget
           appWidgetManager.updateAppWidget(appWidgetId, views);
       }
   }


   @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
   private void setRemoteContentDescription(RemoteViews views, String description) {
       views.setContentDescription(R.id.widget_icon, description);
   }
}




Choose the Size od widget
the widget can chang the size,and show differnt layout


how can we kown what layout do we use?



step1,we create three layout


step2 change the widget_info_today.xml
Although we create 3 layout,but we can’t chnage the size of the widget,how can  we make it can change the size?
the orginal content


<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   android:initialLayout="@layout/widget_today_small"
   android:minHeight="@dimen/widget_today_default_height"
   android:minWidth="@dimen/widget_today_default_width"
   android:updatePeriodMillis="0" />



minWidth and minHeight is the default value of the widget


add android:resizeMode="horizontal"


now,it can resize horizontal


add:android:minResizeWidth="40dp"


and change android:minWidth="110dp"


so the default width is 2box(110dp)
the miniwidth after resize is 1 box(40dp)

finall code:


<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   android:initialLayout="@layout/widget_today_small"
   android:minHeight="@dimen/widget_today_default_height"
   android:minWidth="110dp"
   android:resizeMode="horizontal"
   android:minResizeWidth="@dimen/widget_today_default_width"
   android:updatePeriodMillis="0" />

step3 load different layout from depend on the size of the width
override the method onAppWidgetOptionsChanged

4.0?
no way


4.1+:
int i = newOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH); can get the width


so,now we can get different layout now.
the concept is use the Intent the width to the service,so,the servcie know the width,than it can use the width to load different data and different layout