Fix widget loading and Loader leaks

Change-Id: I1b5a80383b3c56574598298683b4f88afe8121c0
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7701349..58851e1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1446,4 +1446,7 @@
 
     <!-- Label for the widget that shows picture and social status of a contact [CHAR LIMIT=20] -->
     <string name="social_widget_label">Contact</string>
+
+    <!-- Message of widget while it is loading data [CHAR LIMIT=20] -->
+    <string name="social_widget_loading">Loading \u2026</string>
 </resources>
diff --git a/res/xml/social_widget_info.xml b/res/xml/social_widget_info.xml
index 43b1922..4fb849c 100644
--- a/res/xml/social_widget_info.xml
+++ b/res/xml/social_widget_info.xml
@@ -14,10 +14,12 @@
      limitations under the License.
 -->
 
+ <!-- It is enough to update once per day, as the widget watches the database for changes -->
+ 
 <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
     android:minWidth="220dip"
     android:minHeight="72dip"
-    android:updatePeriodMillis="3600000"
+    android:updatePeriodMillis="86400000"
     android:initialLayout="@layout/social_widget"
     android:configure="com.android.contacts.socialwidget.SocialWidgetConfigureActivity" >
 </appwidget-provider>
diff --git a/src/com/android/contacts/socialwidget/SocialWidgetConfigureActivity.java b/src/com/android/contacts/socialwidget/SocialWidgetConfigureActivity.java
index f5033fe..0d63036 100644
--- a/src/com/android/contacts/socialwidget/SocialWidgetConfigureActivity.java
+++ b/src/com/android/contacts/socialwidget/SocialWidgetConfigureActivity.java
@@ -46,10 +46,10 @@
 
             // Save the setting
             final SocialWidgetConfigureActivity context = SocialWidgetConfigureActivity.this;
-            SocialWidgetSettings.setContactUri(context, widgetId, data.getData());
+            SocialWidgetSettings.getInstance().setContactUri(context, widgetId, data.getData());
 
             // Update the widget
-            SocialWidgetProvider.startLoading(context, widgetId);
+            SocialWidgetProvider.loadWidgetData(context, widgetId);
 
             // Return OK so that the system won't remove the widget
             final Intent resultValue = new Intent();
diff --git a/src/com/android/contacts/socialwidget/SocialWidgetProvider.java b/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
index f2641fa..591ca87 100644
--- a/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
+++ b/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
@@ -31,117 +31,146 @@
 import android.provider.ContactsContract.QuickContact;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.View;
 import android.widget.RemoteViews;
 
 public class SocialWidgetProvider extends AppWidgetProvider {
     private static final String TAG = "SocialWidgetProvider";
+    private static SparseArray<ContactLoader> sLoaders = new SparseArray<ContactLoader>();
 
     @Override
     public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
         for (int appWidgetId : appWidgetIds) {
-            startLoading(context, appWidgetId);
+            Log.d(TAG, "onUpdate called for " + appWidgetId);
+        }
+
+        for (int appWidgetId : appWidgetIds) {
+            loadWidgetData(context, appWidgetId);
         }
     }
 
-    public static void startLoading(final Context context, final int widgetId) {
-        // Show that we are loading
-        final AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
-        final RemoteViews loadingViews =
-            new RemoteViews(context.getPackageName(), R.layout.social_widget);
-        loadingViews.setTextViewText(R.id.name, "Loading...");
-        widgetManager.updateAppWidget(widgetId, loadingViews);
-
-        // Load
-        final Uri contactUri = SocialWidgetSettings.getContactUri(context, widgetId);
-        if (contactUri == null) {
-            // Not yet set-up (this can happen while the Configuration activity is visible)
-            return;
+    @Override
+    public void onDeleted(Context context, int[] appWidgetIds) {
+        for (int appWidgetId : appWidgetIds) {
+            ContactLoader loader = sLoaders.get(appWidgetId);
+            if (loader != null) {
+                Log.d(TAG, "Stopping loader for widget with id=" + appWidgetId);
+                loader.stopLoading();
+                sLoaders.delete(appWidgetId);
+            }
         }
-        final ContactLoader contactLoader = new ContactLoader(context, contactUri);
-        contactLoader.registerListener(0,
-                new ContactLoader.OnLoadCompleteListener<ContactLoader.Result>() {
-                    @Override
-                    public void onLoadComplete(Loader<ContactLoader.Result> loader,
-                            ContactLoader.Result contactData) {
-                        if (contactData == ContactLoader.Result.ERROR ||
-                                contactData == ContactLoader.Result.NOT_FOUND) {
-                            return;
+        SocialWidgetSettings.getInstance().remove(context, appWidgetIds);
+    }
+
+    public static void loadWidgetData(final Context context, final int widgetId) {
+        final ContactLoader previousLoader = sLoaders.get(widgetId);
+
+        if (previousLoader != null) {
+            previousLoader.startLoading();
+        } else {
+            // Show that we are loading
+            final AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
+            final RemoteViews loadingViews =
+                    new RemoteViews(context.getPackageName(), R.layout.social_widget);
+            loadingViews.setTextViewText(R.id.name,
+                    context.getString(R.string.social_widget_loading));
+            widgetManager.updateAppWidget(widgetId, loadingViews);
+
+            // Load
+            final Uri contactUri =
+                    SocialWidgetSettings.getInstance().getContactUri(context, widgetId);
+            if (contactUri == null) {
+                // Not yet set-up (this can happen while the Configuration activity is visible)
+                return;
+            }
+            final ContactLoader contactLoader = new ContactLoader(context, contactUri);
+            contactLoader.registerListener(0,
+                    new ContactLoader.OnLoadCompleteListener<ContactLoader.Result>() {
+                        @Override
+                        public void onLoadComplete(Loader<ContactLoader.Result> loader,
+                                ContactLoader.Result contactData) {
+                            if (contactData == ContactLoader.Result.ERROR ||
+                                    contactData == ContactLoader.Result.NOT_FOUND) {
+                                return;
+                            }
+                            Log.d(TAG, "Loaded " + contactData.getLookupKey()
+                                    + " for widget with id=" + widgetId);
+                            final RemoteViews views = new RemoteViews(context.getPackageName(),
+                                    R.layout.social_widget);
+
+                            setDisplayName(views, contactData.getDisplayName(),
+                                    contactData.getPhoneticName());
+                            final Bitmap bitmap = ContactBadgeUtil.getPhoto(contactData);
+                            setPhoto(views, bitmap == null
+                                    ? ContactBadgeUtil.loadPlaceholderPhoto(context) : bitmap);
+                            setSocialSnippet(views, contactData.getSocialSnippet());
+                            setStatusAttribution(views, ContactBadgeUtil.getSocialDate(
+                                    contactData, context));
+
+                            // OnClick launch QuickContact
+                            final Intent intent = new Intent(QuickContact.ACTION_QUICK_CONTACT);
+                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                                    | Intent.FLAG_ACTIVITY_CLEAR_TOP
+                                    | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+
+                            intent.setData(contactData.getLookupUri());
+                            intent.putExtra(QuickContact.EXTRA_MODE, QuickContact.MODE_SMALL);
+
+                            final PendingIntent pendingIntent = PendingIntent.getActivity(context,
+                                    0, intent, 0);
+                            views.setOnClickPendingIntent(R.id.image, pendingIntent);
+
+                            // Configure Ui
+                            widgetManager.updateAppWidget(widgetId, views);
                         }
-                        Log.d(TAG, "Loaded " + contactData.getLookupKey());
-                        final RemoteViews views = new RemoteViews(context.getPackageName(),
-                                R.layout.social_widget);
 
-                        setDisplayName(views, contactData.getDisplayName(),
-                                contactData.getPhoneticName());
-                        final Bitmap bitmap = ContactBadgeUtil.getPhoto(contactData);
-                        setPhoto(views, bitmap == null
-                                ? ContactBadgeUtil.loadPlaceholderPhoto(context) : bitmap);
-                        setSocialSnippet(views, contactData.getSocialSnippet());
-                        setStatusAttribution(views, ContactBadgeUtil.getSocialDate(
-                                contactData, context));
-
-                        // OnClick launch QuickContact
-                        final Intent intent = new Intent(QuickContact.ACTION_QUICK_CONTACT);
-                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                                | Intent.FLAG_ACTIVITY_CLEAR_TOP
-                                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
-
-                        intent.setData(contactData.getLookupUri());
-                        intent.putExtra(QuickContact.EXTRA_MODE, QuickContact.MODE_SMALL);
-
-                        final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0,
-                                intent, 0);
-                        views.setOnClickPendingIntent(R.id.image, pendingIntent);
-
-                        // Configure Ui
-                        widgetManager.updateAppWidget(widgetId, views);
-                    }
-
-                    private void setPhoto(RemoteViews views, Bitmap photo) {
-                        views.setImageViewBitmap(R.id.image, photo);
-                    }
-
-                    /**
-                     * Set the display name and phonetic name to show in the header.
-                     */
-                    private void setDisplayName(RemoteViews views, CharSequence displayName,
-                            CharSequence phoneticName) {
-                        if (TextUtils.isEmpty(phoneticName)) {
-                            views.setTextViewText(R.id.name, displayName);
-                        } else {
-                            final String combinedName =
-                                    context.getString(R.string.widget_name_and_phonetic,
-                                    displayName, phoneticName);
-                            views.setTextViewText(R.id.name, combinedName);
+                        private void setPhoto(RemoteViews views, Bitmap photo) {
+                            views.setImageViewBitmap(R.id.image, photo);
                         }
-                    }
 
-                    /**
-                     * Set the social snippet text to display in the header.
-                     */
-                    private void setSocialSnippet(RemoteViews views, CharSequence snippet) {
-                        if (TextUtils.isEmpty(snippet)) {
-                            views.setViewVisibility(R.id.status, View.GONE);
-                        } else {
-                            views.setTextViewText(R.id.status, snippet);
-                            views.setViewVisibility(R.id.status, View.VISIBLE);
+                        /**
+                         * Set the display name and phonetic name to show in the header.
+                         */
+                        private void setDisplayName(RemoteViews views, CharSequence displayName,
+                                CharSequence phoneticName) {
+                            if (TextUtils.isEmpty(phoneticName)) {
+                                views.setTextViewText(R.id.name, displayName);
+                            } else {
+                                final String combinedName =
+                                        context.getString(R.string.widget_name_and_phonetic,
+                                        displayName, phoneticName);
+                                views.setTextViewText(R.id.name, combinedName);
+                            }
                         }
-                    }
 
-                    /**
-                     * Set the status attribution text to display in the header.
-                     */
-                    private void setStatusAttribution(RemoteViews views,
-                            CharSequence attribution) {
-                        if (attribution == null) {
-                            views.setViewVisibility(R.id.status_date, View.GONE);
-                        } else {
-                            views.setTextViewText(R.id.status_date, attribution);
-                            views.setViewVisibility(R.id.status_date, View.VISIBLE);
+                        /**
+                         * Set the social snippet text to display in the header.
+                         */
+                        private void setSocialSnippet(RemoteViews views, CharSequence snippet) {
+                            if (TextUtils.isEmpty(snippet)) {
+                                views.setViewVisibility(R.id.status, View.GONE);
+                            } else {
+                                views.setTextViewText(R.id.status, snippet);
+                                views.setViewVisibility(R.id.status, View.VISIBLE);
+                            }
                         }
-                    }
-                });
-        contactLoader.startLoading();
+
+                        /**
+                         * Set the status attribution text to display in the header.
+                         */
+                        private void setStatusAttribution(RemoteViews views,
+                                CharSequence attribution) {
+                            if (attribution == null) {
+                                views.setViewVisibility(R.id.status_date, View.GONE);
+                            } else {
+                                views.setTextViewText(R.id.status_date, attribution);
+                                views.setViewVisibility(R.id.status_date, View.VISIBLE);
+                            }
+                        }
+                    });
+            contactLoader.startLoading();
+            sLoaders.append(widgetId, contactLoader);
+        }
     }
 }
diff --git a/src/com/android/contacts/socialwidget/SocialWidgetSettings.java b/src/com/android/contacts/socialwidget/SocialWidgetSettings.java
index 8469d61..18b5041 100644
--- a/src/com/android/contacts/socialwidget/SocialWidgetSettings.java
+++ b/src/com/android/contacts/socialwidget/SocialWidgetSettings.java
@@ -26,16 +26,30 @@
     private static final String TAG = "SocialWidgetSettings";
 
     private static final String PREFS_NAME = "WidgetSettings";
-    private static final String CONTACT_URI = "CONTACT_URI_%";
+    private static final String CONTACT_URI_PREFIX = "CONTACT_URI_";
 
-    private static String getSettingsString(int widgetId) {
-        return CONTACT_URI.replace("%", Integer.toString(widgetId));
+    private static final SocialWidgetSettings sInstance = new SocialWidgetSettings();
+
+    public static SocialWidgetSettings getInstance() {
+        return sInstance;
     }
 
-    // TODO: Think about how to remove not-used Ids...we need a way to detect removed
-    // widgets
+    private final String getSettingsString(int widgetId) {
+        return CONTACT_URI_PREFIX + Integer.toString(widgetId);
+    }
 
-    public static Uri getContactUri(Context context, int widgetId) {
+    public void remove(Context context, int[] widgetIds) {
+        final SharedPreferences settings =
+            context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+        final Editor editor = settings.edit();
+        for (int widgetId : widgetIds) {
+            Log.d(TAG, "remove(" + widgetId + ")");
+            editor.remove(getSettingsString(widgetId));
+        }
+        editor.apply();
+    }
+
+    public Uri getContactUri(Context context, int widgetId) {
         final SharedPreferences settings =
                 context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
         final String resultString = settings.getString(getSettingsString(widgetId), null);
@@ -44,7 +58,7 @@
         return result;
     }
 
-    public static void setContactUri(Context context, int widgetId, Uri contactLookupUri) {
+    public void setContactUri(Context context, int widgetId, Uri contactLookupUri) {
         Log.d(TAG, "setContactUri(" + widgetId + ", " + contactLookupUri + ")");
         final SharedPreferences settings =
                 context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);