1

Quick explanation of the app. It is a simple widget where 4 values are saved via SharedPreferences. The only activity view is the place where the preferences are set. Once done, the variables are saved, and the widget is updated. The widget updates on it's own every 90,000ms. This is all working fine. The problem I have is when the orientation changes. When that happens, the widget is destroyed, and then remade. What I get is the raw XML with the variable Text fields set to default. It stays like this until the next manual or automatic update.

EDIT: This question is now altered considerably. The two responses that I got pointed out a few things that were wrong. Read all the comments to see all of that. After the last attempt, which was to try implenting a tag in the in the manifest, I got even worse results. The program simply failed to run at all.

I'm going with the last thing said in the same answer that provided that idea. There must be something wrong with my onUpdate(). Here is the code for the entire class:

public class WatchWidget extends AppWidgetProvider
{

    public void onCreate(Context context, Bundle savedInstanceState) 
    {

        RemoteViews remoteViews;
        remoteViews = new RemoteViews( context.getPackageName(), R.layout.main );
        remoteViews.setTextViewText( R.id.widget_elapsedtime, "Time");

    }

    @Override
    public void onUpdate( Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
    {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        RemoteViews remoteViews;
        ComponentName watchWidget;
        DateFormat format = SimpleDateFormat.getTimeInstance( SimpleDateFormat.MEDIUM, Locale.getDefault() );

        remoteViews = new RemoteViews( context.getPackageName(), R.layout.main );
        watchWidget = new ComponentName( context, WatchWidget.class );
        //remoteViews.setTextViewText( R.id.widget_textview, "" + format.format( new Date()));

        //widget_date
        Calendar c = Calendar.getInstance();
        SimpleDateFormat df = new SimpleDateFormat("MM/dd/yyyy");
        String formattedDate = df.format(c.getTime());
        //remoteViews.setTextViewText( R.id.widget_date, formattedDate); 

        SharedPreferences prefs = context.getSharedPreferences("qsPrefs", context.MODE_PRIVATE);
        String sdayTextEdit = prefs.getString("dayTextEdit", "23");
        //String sdayTextEdit ="23";
        String smonthTextEdit = prefs.getString("monthTextEdit", "3");
        String syearTextEdit = prefs.getString("yearTextEdit", "2014");
        String scostTextEdit = prefs.getString("costTextEdit", "8.5");
        String spacksTextEdit = prefs.getString("packsTextEdit", ".95");


        //Starting day
        Calendar pastdate = Calendar.getInstance();
        //int inputDay = 25;
        int inputDay = Integer.valueOf(sdayTextEdit);
        int inputMonth = 2;  //starts at 0
        inputMonth = Integer.valueOf(smonthTextEdit);
        int inputMonthAdj = inputMonth-1;
        int inputYear = 2014;
        inputYear = Integer.valueOf(syearTextEdit);
        pastdate.set(Calendar.DAY_OF_MONTH, inputDay);
        pastdate.set(Calendar.MONTH, inputMonthAdj);
        pastdate.set(Calendar.YEAR, inputYear);
        double packsPer = .95;
        packsPer = Double.parseDouble(spacksTextEdit.toString());
        double perPack = 8.57;
        perPack = Double.parseDouble(scostTextEdit.toString());

        inputMonthAdj = inputMonth;
        remoteViews.setTextViewText( R.id.widget_date, "From:  " + inputMonthAdj +"/"+ inputDay +"/"+ inputYear );

        SfinalDate = "From:  " + inputMonthAdj +"/"+ inputDay +"/"+ inputYear;


        Calendar today = Calendar.getInstance();
        DecimalFormat dform = new DecimalFormat("0.00");

        long dateDiff = today.getTimeInMillis() - pastdate.getTimeInMillis();

        long resultDays = dateDiff / (24*60*60*1000);
        remoteViews.setTextViewText( R.id.widget_elapsedtime, ""+ resultDays);

        double savedmoney = packsPer * perPack * resultDays;
        String finalMoney = dform.format(savedmoney);
        remoteViews.setTextViewText( R.id.widget_savedmoney, "$"+ finalMoney);

        double notSmoked = resultDays * 20 * packsPer;
        int finalNotSmoked = (int) Math.round(notSmoked);
        remoteViews.setTextViewText( R.id.widget_cigsnot, ""+ finalNotSmoked);

        SfinalMoney = "$"+ finalMoney;
        SfinalNS = ""+ finalNotSmoked;
        SfinalDays = ""+ resultDays;

        appWidgetManager.updateAppWidget( watchWidget, remoteViews );

        //Show Prefs screen
        Intent intent = new Intent(context, preferences.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.main);
        views.setOnClickPendingIntent(R.id.LinearLayout01, pendingIntent);
        appWidgetManager.updateAppWidget( watchWidget, views );
    }

}

Having posted that... There may be a few extra lines attempting to set things that are no longer relavent. But the code as it is there works. The onUpdate() fires fine at the automatic interval (90000ms). And the activity that launches to set the preferences, also makes the call when it closes, which also updates it fine. This is what has led me to believe that the entire void must be ok. But there must be something amiss. Because when the screen orientation changes, the onUpdate() does not run. And, again, as per the second answer, it shouldn't have to anyway, because the system is supposed to retain the UI on it's own. But it's not.

durbnpoisn
  • 4,666
  • 2
  • 16
  • 30
  • Your widget provider's `onUpdate()` should be called. In `onUpdate()` you should set the Views state according to what you need. – Yaroslav Mytkalyk Apr 23 '14 at 14:27
  • I tried that. But I don't know how to get it to fire. That is, I tried setting it to a super.onUpdate, so I could call it at any time. But I got the same error as described for all the other supers. And besides, there doesn't seem to be a place I can KNOW will be triggered to call it from. – durbnpoisn Apr 23 '14 at 14:35
  • Are you saying that `onUpdate()` is not triggered when the orientation is changed? You can try overriding `onAppWidgetOptionsChanged()` and updating the widget state from there as well as from `onUpdate()` – Yaroslav Mytkalyk Apr 23 '14 at 14:46
  • onUpdate() is not called on orientation change. It is only called as per the defined XML tag in AppWidgetProvider. I thought that onCreate() would fire, but I don't see THAT happening either. I'm not using onAppWidgetOptionsChanged() for anything. The preferences I'm saving are in a custom activity. – durbnpoisn Apr 23 '14 at 14:53

2 Answers2

4

What you are trying to do works for an Activity but not for a BroadcastReceiver. An AppWidgetProvider is a BroadcastReceiver and doesn't have the methods you are trying to override (onSaveInstanceState, onRestoreInstanceState). If you add the @Override annotation to the methods it tells you

The method xyz() of type YourWidgetProvider must override or implement a supertype method

That's because the AppWidgetProvider class doesn't have a method onSaveInstanceState or onRestoreInstanceState and you can't override what's not there. Just removing the @Override annotation will get rid of the compile error but you'll just add a method that is never called (unless you do it yourself of course but that's pointless).

The way to solve this problem is to listen for configuration changes in the Application and broadcast an ACTION_APPWIDGET_UPDATE to the widget.

Add an Application class to your app

public class MyApplication extends Application {

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        // create intent to update all instances of the widget
        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, this, MyWidgetProvider.class);

        // retrieve all appWidgetIds for the widget & put it into the Intent
        AppWidgetManager appWidgetMgr = AppWidgetManager.getInstance(this);
        ComponentName cm = new ComponentName(this, MyWidgetProvider.class);
        int[] appWidgetIds = appWidgetMgr.getAppWidgetIds(cm);
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);

        // update the widget
        sendBroadcast(intent);
    }
}

The method will be called whenever a configuration change occurs, one of them being an orientation change. You could improve the method by doing the broadcast only when it's an orientation change and not e.g. a change of the system language. The method basically sends an update broadcast to all instances of your widget (replace MyWidgetProvider by whatever name you use for your widget provider).

Define the MyApplication in your manifest

<application
    android:name="yourpackagename.MyApplication"
    android:description="@string/app_name"
    android:label="@string/app_name"
    android:icon="@drawable/app_icon">

    <!-- here go your Activity definitions -->

</application>

Important this is only necessary if you use different layouts for landscape and portrait. It's NOT necessary otherwise because Android takes care of saving and restoring the widget's ui state. If you get a widget with default values then you are doing something wrong in your onUpdate method. Now I had a look at your onUpdate method and there's one major issue (there are others but I'll point out only the one that will definitely break your widget).

You create new RemoteViews after you already called the updateAppWidget with the initialized TextViews:

appWidgetManager.updateAppWidget( watchWidget, remoteViews );

//Show Prefs screen
Intent intent = new Intent(context, preferences.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

// this line gets rid of all the work you have done above
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.main);

views.setOnClickPendingIntent(R.id.LinearLayout01, pendingIntent);
appWidgetManager.updateAppWidget( watchWidget, views );

So after creating the views and setting the values you throw it all away and create new RemoteViews which obviously have empty/uninitialized TextViews. It's also unnecessary to call the appWidgetManager.updateAppWidget twice, once per onUpdate call is enough. So what you do is this:

remoteViews.setTextViewText( R.id.widget_cigsnot, ""+ finalNotSmoked);

SfinalMoney = "$"+ finalMoney;
SfinalNS = ""+ finalNotSmoked;
SfinalDays = ""+ resultDays;

//Show Prefs screen
Intent intent = new Intent(context, preferences.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
remoteViews.setOnClickPendingIntent(R.id.LinearLayout01, pendingIntent);
appWidgetManager.updateAppWidget( appWidgetIds, remoteViews );

Please note that I'm using updateAppWidget(int[] appWidgetIds, RemoteViews views) instead of updateAppWidget(ComponentName provider, RemoteViews views) because that allows you to update just certain widget instances and you don't need the ComponentName which saves you another line of code.

Emanuel Moecklin
  • 28,488
  • 11
  • 69
  • 85
  • Everything you have hear appears to make good sense. And I will give it a shot later today. This sort of seems like another suggestion like handling it as a Service. I had no luck with that either. As far as the onUpdate(), I can't see that there is anything wrong with it. If you look at the other answer in this thread, that IS exactly what mine is doing (so the code is right there). And it works for the automatic and manual calls. Why the system destroys it and rebuilds it as default without running the update, I do no know. – durbnpoisn Apr 25 '14 at 10:54
  • I tried to implement this exaclty as suggested. The code compiled. But the app failed to run - no explanation in the log cat. It just doesn't run at all. So I've updated the original question to include the entire class where the onUpdate() is running. I'm going on the assumption now that your last comment, that the UI should be maintained but is not, must be true. So I'm including that code in hopes that you can point out my flaw. Thank you! – durbnpoisn Apr 25 '14 at 13:23
  • I updated my answer to explain why your onUpdate fails – Emanuel Moecklin Apr 25 '14 at 15:40
  • Eggsellent! I'll give this a shot. I'll let you know how it goes. – durbnpoisn Apr 25 '14 at 16:29
  • Thank you. After all of that it really was a mistake in my code. You win the bounty! But now I have a new problem. The widget is not updating after the preference change. If I can't figure that out, I'll create a new question. – durbnpoisn Apr 25 '14 at 22:50
  • I do still have a minor issue. One change you suggested was to change the line "appWidgetManager.updateAppWidget( watchWidget, remoteViews );" to "appWidgetManager.updateAppWidget( appWidgetIds, remoteViews );". The difference in these methods is that it's accessing the IDs rather than a defined Component. This works when the widget is updated (manually or auto). But once the orientation change occurs, the "setOnClickPendingIntent" for goes away. It fixes itself and works again on the next update. But I don't understand why it gets lost otherwise. – durbnpoisn Apr 29 '14 at 14:35
  • It's all there. The Compenent declaration is in the code I posted before. The new method for appWidgetManager is exactly what you suggested. The thing is, the Compenent method worked before. The IDs method only works for Manual and Auto updates. It's like when the screen changes orientation, the listener goes away. This is only about the calling of the Activity to set preferences. Everything else is working 100%. – durbnpoisn Apr 29 '14 at 14:42
  • Well, yeah. I took your advice to go by IDs, thinking that was the better method. I'll still take your advice on this. Which do you think the the best method? Will a Component handle multiple instances? I'm assuming that's why IDs was suggested at all. – durbnpoisn Apr 29 '14 at 17:45
  • The id method allows to update just certain instances of the widget while the component method would update all of them. If all the widgets show the same content use the component method. – Emanuel Moecklin Apr 29 '14 at 18:15
  • They do show the same content, as they are all reading from the same data. That answers that then. Apparently that was one thing I had right! Thank you very much. – durbnpoisn Apr 29 '14 at 19:27
0

onCreate() of AppWidgetProvider is called only when first widget is created. Widget should be updated in onUpdate(), not from the Activity.

The widget's lifecycle has nothing to do with Activity's lifecycle.

@Override
public void onUpdate(final Context context,
        final AppWidgetManager appWidgetManager,
        final int[] appWidgetIds) {

    udpateViews(context, appWidgetManager, appWidgetIds);
}

@Override
public void onAppWidgetOptionsChanged(final Context context,
        final AppWidgetManager appWidgetManager, final int appWidgetId, final Bundle newOptions) {

    updateViews(context, appWidgetManager, appWidgetId);
}

private static void udpateViews(final Context context,
        final AppWidgetManager appWidgetManager,
        final int... appWidgetIds) {

    final String money = // read from shared prefs
    final String ns = // read from shared prefs
    final String days = // read from shared prefs
    final String date = // read from shared prefs

    final RemoteViews views = remoteViews = new RemoteViews(context.getPackageName(), R.layout.main);
    remoteViews.setTextViewText(R.id.widget_cigsnot, ns);
    remoteViews.setTextViewText(R.id.widget_savedmoney, money);
    remoteViews.setTextViewText(R.id.widget_elapsedtime, days);
    remoteViews.setTextViewText(R.id.widget_date, date);

    //always call updateAppWidget whenever RemoteViews had changed
    appWidgetManager.updateAppWidget(appWidgetIds, views);
}
Yaroslav Mytkalyk
  • 16,950
  • 10
  • 72
  • 99
  • Okay. This looks like at least a couple of things I have not yet tried. And since this appears to avoid using "super"s, I may have better success. I will give this a shot when I get home tonight. Thank you! – durbnpoisn Apr 23 '14 at 15:20
  • I set this all up as suggested. And, yes, the idea here of moving all the actual updates to its own separate void is a great idea, and works well - it still doesn't solve my problem. It seems that "void onAppWidgetOptionsChanged" is not firing when orientation changes. I even set up a little Toast alert to let me know if it fires. It doesn't. Is there somewhere else it needs to be declared? – durbnpoisn Apr 24 '14 at 02:29
  • Searching around I found that the onUpdate() is really not called on home screen widgets (I've never tried my widgets on launchers that can change orientation). Try using the Service then http://stackoverflow.com/questions/3503114/after-orientation-change-buttons-on-a-widget-are-not-responding – Yaroslav Mytkalyk Apr 24 '14 at 08:12
  • I'll try that next, I guess. But does anyone have any idea why things like "void onRestoreInstanceState()" won't work, with the super declared? I still think that is a more reasonable option, considering it's exactly what it's for. But I cannot get it to compile. – durbnpoisn Apr 25 '14 at 00:48
  • onRestoreInstanceState is an Activity component. What does it have to do with widgets? Also, super implementation of onRestoreInstanceState of Activity does nothing. It's just a callback for a developer to handle restoring state if restoring in onCreate() is not preferred. – Yaroslav Mytkalyk Apr 25 '14 at 09:08
  • In all of my searching to solve this issue, that was just one of the ideas that I'd tried. And I never got an explanation for why it wasn't going to work until now. Thank you for you help, as you are helping to narrow the situation for me. – durbnpoisn Apr 25 '14 at 10:47