Enable calendar sync for Exchange accounts when upgrading.

On the first boot after upgrade from Eclair, enable calendar sync for all the
existing Exchange accounts, if any, and show notification.

Note on this version, nothing happens when you click on the "Calendar added"
notification.  We're waiting for an API (action or something) to launch
calendar.

Bug 2428718
diff --git a/res/drawable-hdpi/stat_notify_calendar.png b/res/drawable-hdpi/stat_notify_calendar.png
new file mode 100644
index 0000000..7e7cd60
--- /dev/null
+++ b/res/drawable-hdpi/stat_notify_calendar.png
Binary files differ
diff --git a/res/drawable-mdpi/stat_notify_calendar.png b/res/drawable-mdpi/stat_notify_calendar.png
new file mode 100755
index 0000000..4433a16
--- /dev/null
+++ b/res/drawable-mdpi/stat_notify_calendar.png
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a46fc74..caee97c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -644,4 +644,8 @@
     <!-- Long-form description of the DeviceAdmin (2nd line in settings & in user conf. screen) -->
     <string name="device_admin_description">Enables server-specified security policies</string>
 
+    <!-- Notification message in notifications window when calendar sync is
+         automatically enabled for pre-existing Exchange accounts on upgrade -->
+    <string name="notification_exchange_calendar_added">Exchange calendar added</string>
+
 </resources>
diff --git a/src/com/android/email/ExchangeUtils.java b/src/com/android/email/ExchangeUtils.java
index 701de73..a33613c 100644
--- a/src/com/android/email/ExchangeUtils.java
+++ b/src/com/android/email/ExchangeUtils.java
@@ -20,6 +20,7 @@
 import com.android.email.service.EmailServiceProxy;
 import com.android.email.service.IEmailService;
 import com.android.email.service.IEmailServiceCallback;
+import com.android.exchange.CalendarSyncEnabler;
 import com.android.exchange.SyncManager;
 
 import android.content.Context;
@@ -61,6 +62,15 @@
     }
 
     /**
+     * Enable calendar sync for all the existing exchange accounts, and post a notification if any.
+     */
+    public static void enableEasCalendarSync(Context context) {
+        //EXCHANGE-REMOVE-SECTION-START
+        new CalendarSyncEnabler(context).enableEasCalendarSync();
+        //EXCHANGE-REMOVE-SECTION-END
+    }
+
+    /**
      * An empty {@link IEmailService} implementation which is used instead of
      * {@link com.android.exchange.SyncManager} on the build with no exchange support.
      *
diff --git a/src/com/android/email/OneTimeInitializer.java b/src/com/android/email/OneTimeInitializer.java
index 24c49cb..c49f586 100644
--- a/src/com/android/email/OneTimeInitializer.java
+++ b/src/com/android/email/OneTimeInitializer.java
@@ -59,6 +59,8 @@
                 setComponentEnabled(context, EasAuthenticatorServiceAlternate.class, true);
                 setComponentEnabled(context, EasAuthenticatorService.class, false);
             }
+
+            ExchangeUtils.enableEasCalendarSync(context);
         }
 
         // If we need other initializations in the future...
diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java
index ed2c361..eda0df7 100644
--- a/src/com/android/email/service/MailService.java
+++ b/src/com/android/email/service/MailService.java
@@ -55,6 +55,7 @@
 

     public static int NOTIFICATION_ID_NEW_MESSAGES = 1;

     public static int NOTIFICATION_ID_SECURITY_NEEDED = 2;

+    public static int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 3;

 

     private static final String ACTION_CHECK_MAIL =

         "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";

diff --git a/src/com/android/exchange/CalendarSyncEnabler.java b/src/com/android/exchange/CalendarSyncEnabler.java
new file mode 100644
index 0000000..d8bc919
--- /dev/null
+++ b/src/com/android/exchange/CalendarSyncEnabler.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2010 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.android.exchange;
+
+import com.android.email.Email;
+import com.android.email.R;
+import com.android.email.service.MailService;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Calendar;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+/**
+ * Utility class to enable Exchange calendar sync for all existing Exchange accounts.
+ *
+ * <p>Exchange calendar was first supported on Froyo.  It wasn't supported on Eclair, which
+ * was the first version that supported Exchange email.
+ *
+ * <p>This class is used only once when the devices is upgraded to Froyo (or later) from Eclair,
+ * to enable calendar sync for all the existing Exchange accounts.
+ */
+public class CalendarSyncEnabler {
+    private final Context mContext;
+
+    public CalendarSyncEnabler(Context context) {
+        this.mContext = context;
+    }
+
+    /**
+     * Enable calendar sync for all the existing exchange accounts, and post a notification if any.
+     */
+    public final void enableEasCalendarSync() {
+        String emailAddresses = enableEasCalendarSyncInternal();
+        if (emailAddresses.length() > 0) {
+            // Exchange account(s) found.
+            showNotification(emailAddresses.toString());
+        }
+    }
+
+    /**
+     * Enable calendar sync for all the existing exchange accounts
+     *
+     * @return email addresses of the Exchange accounts joined with spaces as delimiters,
+     *     or the empty string if there's no Exchange accounts.
+     */
+    /* package for testing */ final String enableEasCalendarSyncInternal() {
+        StringBuilder emailAddresses = new StringBuilder();
+
+        Account[] exchangeAccounts = AccountManager.get(mContext)
+                .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+        for (Account account : exchangeAccounts) {
+            final String emailAddress = account.name;
+            Log.i(Email.LOG_TAG, "Enabling Exchange calendar sync for " + emailAddress);
+
+            ContentResolver.setIsSyncable(account, Calendar.AUTHORITY, 1);
+            ContentResolver.setSyncAutomatically(account, Calendar.AUTHORITY, true);
+
+            // Accumulate addresses for notification.
+            if (emailAddresses.length() > 0) {
+                emailAddresses.append(' ');
+            }
+            emailAddresses.append(emailAddress);
+        }
+        return emailAddresses.toString();
+    }
+
+    /**
+     * Show the "Exchange calendar added" notification.
+     *
+     * @param emailAddresses space delimited list of email addresses of Exchange accounts.  It'll
+     *     be shown on the notification.
+     */
+    /* package for testing */ void showNotification(String emailAddresses) {
+        // TODO We don't have an intent to launch calendar yet.  Change it when we have.
+        Intent calendarIntent = new Intent().setAction("TODO.change.this.to.launch.calendar");
+
+        // Launch Calendar app when clicked.
+        PendingIntent launchCalendarPendingIntent = PendingIntent.getActivity(mContext, 0,
+                calendarIntent, 0);
+
+        String tickerText = mContext.getString(R.string.notification_exchange_calendar_added);
+        Notification n = new Notification(R.drawable.stat_notify_calendar,
+                tickerText, System.currentTimeMillis());
+        n.setLatestEventInfo(mContext, tickerText, emailAddresses, launchCalendarPendingIntent);
+        n.flags = Notification.FLAG_AUTO_CANCEL;
+
+        NotificationManager nm =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        nm.notify(MailService.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED, n);
+    }
+}
diff --git a/tests/src/com/android/email/AccountTestCase.java b/tests/src/com/android/email/AccountTestCase.java
index 69dfe3c..273581b 100644
--- a/tests/src/com/android/email/AccountTestCase.java
+++ b/tests/src/com/android/email/AccountTestCase.java
@@ -24,7 +24,6 @@
 import android.accounts.AccountManagerFuture;
 import android.accounts.AuthenticatorException;
 import android.accounts.OperationCanceledException;
-import android.content.Context;
 import android.database.Cursor;
 import android.test.ProviderTestCase2;
 
@@ -40,8 +39,13 @@
     protected static final String TEST_ACCOUNT_PREFIX = "__test";
     protected static final String TEST_ACCOUNT_SUFFIX = "@android.com";
 
-    public AccountTestCase(Class<EmailProvider> providerClass, String providerAuthority) {
-        super(providerClass, providerAuthority);
+    public AccountTestCase() {
+        super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
+    }
+
+    protected android.accounts.Account[] getExchangeAccounts() {
+        return AccountManager.get(getContext())
+                .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
     }
 
     protected android.accounts.Account makeAccountManagerAccount(String username) {
@@ -74,9 +78,9 @@
         return accountList;
     }
 
-    protected void deleteAccountManagerAccount(Context context, android.accounts.Account account) {
+    protected void deleteAccountManagerAccount(android.accounts.Account account) {
         AccountManagerFuture<Boolean> future =
-            AccountManager.get(context).removeAccount(account, null, null);
+            AccountManager.get(getContext()).removeAccount(account, null, null);
         try {
             future.getResult();
         } catch (OperationCanceledException e) {
@@ -85,13 +89,11 @@
         }
     }
 
-    protected void deleteTemporaryAccountManagerAccounts(Context context) {
-        android.accounts.Account[] accountManagerAccounts =
-                AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
-        for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
+    protected void deleteTemporaryAccountManagerAccounts() {
+        for (android.accounts.Account accountManagerAccount: getExchangeAccounts()) {
             if (accountManagerAccount.name.startsWith(TEST_ACCOUNT_PREFIX) &&
                     accountManagerAccount.name.endsWith(TEST_ACCOUNT_SUFFIX)) {
-                deleteAccountManagerAccount(context, accountManagerAccount);
+                deleteAccountManagerAccount(accountManagerAccount);
             }
         }
     }
@@ -109,10 +111,9 @@
      * Helper to retrieve account manager accounts *and* remove any preexisting accounts
      * from the list, to "hide" them from the reconciler.
      */
-    protected android.accounts.Account[] getAccountManagerAccounts(Context context,
+    protected android.accounts.Account[] getAccountManagerAccounts(
             android.accounts.Account[] baseline) {
-        android.accounts.Account[] rawList =
-            AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+        android.accounts.Account[] rawList = getExchangeAccounts();
         if (baseline.length == 0) {
             return rawList;
         }
diff --git a/tests/src/com/android/exchange/CalendarSyncEnablerTest.java b/tests/src/com/android/exchange/CalendarSyncEnablerTest.java
new file mode 100644
index 0000000..16b9b28
--- /dev/null
+++ b/tests/src/com/android/exchange/CalendarSyncEnablerTest.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2010 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.android.exchange;
+
+import com.android.email.AccountTestCase;
+import com.android.email.Email;
+import com.android.email.service.MailService;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.NotificationManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.provider.Calendar;
+import android.test.MoreAsserts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+
+public class CalendarSyncEnablerTest extends AccountTestCase {
+
+    private HashMap<Account, Boolean> origCalendarSyncStates = new HashMap<Account, Boolean>();
+
+    // To make the rest of the code shorter thus more readable...
+    private static final String EAT = Email.EXCHANGE_ACCOUNT_MANAGER_TYPE;
+
+    public CalendarSyncEnablerTest() {
+        super();
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        // Delete any test accounts we might have created earlier
+        deleteTemporaryAccountManagerAccounts();
+
+        // Save the original calendar sync states.
+        for (Account account : AccountManager.get(getContext()).getAccounts()) {
+            origCalendarSyncStates.put(account,
+                    ContentResolver.getSyncAutomatically(account, Calendar.AUTHORITY));
+        }
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        // Delete any test accounts we might have created earlier
+        deleteTemporaryAccountManagerAccounts();
+
+        // Restore the original calendar sync states.
+        // Note we restore only for Exchange accounts.
+        // Other accounts should remain intact throughout the tests.  Plus we don't know if the
+        // Calendar.AUTHORITY is supported by other types of accounts.
+        for (Account account : getExchangeAccounts()) {
+            Boolean state = origCalendarSyncStates.get(account);
+            if (state == null) continue; // Shouldn't happen, but just in case.
+
+            ContentResolver.setSyncAutomatically(account, Calendar.AUTHORITY, state);
+        }
+    }
+
+    public void testEnableEasCalendarSync() {
+        final Account[] baseAccounts = getExchangeAccounts();
+
+        String a1 = getTestAccountEmailAddress("1");
+        String a2 = getTestAccountEmailAddress("2");
+
+        // 1. Test with 1 account
+
+        CalendarSyncEnabler enabler = new CalendarSyncEnabler(getContext());
+
+        // Add exchange accounts
+        createAccountManagerAccount(a1);
+
+        String emailAddresses = enabler.enableEasCalendarSyncInternal();
+
+        // Verify
+        verifyCalendarSyncState();
+
+        // There seems to be no good way to examine the contents of Notification, so let's verify
+        // we at least (tried to) show the correct email addresses.
+        checkNotificationEmailAddresses(emailAddresses, baseAccounts, a1);
+
+        // Delete added account.
+        deleteTemporaryAccountManagerAccounts();
+
+        // 2. Test with 2 accounts
+        enabler = new CalendarSyncEnabler(getContext());
+
+        // Add exchange accounts
+        createAccountManagerAccount(a1);
+        createAccountManagerAccount(a2);
+
+        emailAddresses = enabler.enableEasCalendarSyncInternal();
+
+        // Verify
+        verifyCalendarSyncState();
+
+        // Check
+        checkNotificationEmailAddresses(emailAddresses, baseAccounts, a1, a2);
+    }
+
+    private static void checkNotificationEmailAddresses(String actual, Account[] baseAccounts,
+            String... addedAddresses) {
+        // Build and sort actual string array.
+        final String[] actualArray = TextUtils.split(actual, " ");
+        Arrays.sort(actualArray);
+
+        // Build and sort expected string array.
+        ArrayList<String> expected = new ArrayList<String>();
+        for (Account account : baseAccounts) {
+            expected.add(account.name);
+        }
+        for (String address : addedAddresses) {
+            expected.add(address);
+        }
+        final String[] expectedArray = new String[expected.size()];
+        expected.toArray(expectedArray);
+        Arrays.sort(expectedArray);
+
+        // Check!
+        MoreAsserts.assertEquals(expectedArray, actualArray);
+    }
+
+    /**
+     * For all {@link Account}, confirm that:
+     * <ol>
+     *   <li>Calendar sync is enabled if it's an Exchange account.<br>
+     *       Unfortunately setSyncAutomatically() doesn't take effect immediately, so we skip this
+     *       check for now.
+             TODO Find a stable way to check this.
+     *   <li>Otherwise, calendar sync state isn't changed.
+     * </ol>
+     */
+    private void verifyCalendarSyncState() {
+        // It's very unfortunate that setSyncAutomatically doesn't take effect immediately.
+        for (Account account : AccountManager.get(getContext()).getAccounts()) {
+            String message = "account=" + account.name + "(" + account.type + ")";
+            boolean enabled = ContentResolver.getSyncAutomatically(account, Calendar.AUTHORITY);
+            int syncable = ContentResolver.getIsSyncable(account, Calendar.AUTHORITY);
+
+            if (EAT.equals(account.type)) {
+                // Should be enabled.
+                // assertEquals(message, Boolean.TRUE, (Boolean) enabled);
+                // assertEquals(message, 1, syncable);
+            } else {
+                // Shouldn't change.
+                assertEquals(message, origCalendarSyncStates.get(account), (Boolean) enabled);
+            }
+        }
+    }
+
+    public void testEnableEasCalendarSyncWithNoExchangeAccounts() {
+        // This test can only meaningfully run when there's no exchange accounts
+        // set up on the device.  Otherwise there'll be no difference from
+        // testEnableEasCalendarSync.
+        if (AccountManager.get(getContext()).getAccountsByType(EAT).length > 0) {
+            Log.w(Email.LOG_TAG, "testEnableEasCalendarSyncWithNoExchangeAccounts skipped:"
+                    + " It only runs when there's no Exchange account on the device.");
+            return;
+        }
+        CalendarSyncEnabler enabler = new CalendarSyncEnabler(getContext());
+        String emailAddresses = enabler.enableEasCalendarSyncInternal();
+
+        // Verify (nothing should change)
+        verifyCalendarSyncState();
+
+        // No exchange accounts found.
+        assertEquals(0, emailAddresses.length());
+    }
+
+    public void testShowNotification() {
+        CalendarSyncEnabler enabler = new CalendarSyncEnabler(getContext());
+
+        // We can't really check the result, but at least we can make sure it won't crash....
+        enabler.showNotification("a@b.com");
+
+        // Remove the notification.  Comment it out when you want to know how it looks like.
+        ((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE))
+                .cancel(MailService.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED);
+    }
+}
diff --git a/tests/src/com/android/exchange/SyncManagerAccountTests.java b/tests/src/com/android/exchange/SyncManagerAccountTests.java
index 32c335f..3b67dc1 100644
--- a/tests/src/com/android/exchange/SyncManagerAccountTests.java
+++ b/tests/src/com/android/exchange/SyncManagerAccountTests.java
@@ -43,7 +43,7 @@
     Context mMockContext;
 
     public SyncManagerAccountTests() {
-        super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
+        super();
     }
 
     @Override
@@ -51,14 +51,14 @@
         super.setUp();
         mMockContext = getMockContext();
         // Delete any test accounts we might have created earlier
-        deleteTemporaryAccountManagerAccounts(getContext());
+        deleteTemporaryAccountManagerAccounts();
     }
 
     @Override
     public void tearDown() throws Exception {
         super.tearDown();
         // Delete any test accounts we might have created earlier
-        deleteTemporaryAccountManagerAccounts(getContext());
+        deleteTemporaryAccountManagerAccounts();
     }
 
     /**
@@ -87,7 +87,7 @@
                 for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
                     if ((TEST_USER_ACCOUNT + TEST_ACCOUNT_SUFFIX)
                             .equals(accountManagerAccount.name)) {
-                        deleteAccountManagerAccount(context, accountManagerAccount);
+                        deleteAccountManagerAccount(accountManagerAccount);
                         firstAccountFound = true;
                     }
                 }
@@ -118,16 +118,16 @@
         // Check that they're set up properly
         assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
         android.accounts.Account[] accountManagerAccounts =
-                getAccountManagerAccounts(context, baselineAccounts);
+                getAccountManagerAccounts(baselineAccounts);
         assertEquals(3, accountManagerAccounts.length);
 
         // Delete account "2" from AccountManager
         android.accounts.Account removedAccount =
             makeAccountManagerAccount(getTestAccountEmailAddress("2"));
-        deleteAccountManagerAccount(context, removedAccount);
+        deleteAccountManagerAccount(removedAccount);
 
         // Confirm it's deleted
-        accountManagerAccounts = getAccountManagerAccounts(context, baselineAccounts);
+        accountManagerAccounts = getAccountManagerAccounts(baselineAccounts);
         assertEquals(2, accountManagerAccounts.length);
 
         // Run the reconciler
@@ -149,7 +149,7 @@
                 makeSyncManagerAccountList(), accountManagerAccounts, true, resolver);
 
         // There should now be only one AccountManager account
-        accountManagerAccounts = getAccountManagerAccounts(context, baselineAccounts);
+        accountManagerAccounts = getAccountManagerAccounts(baselineAccounts);
         assertEquals(1, accountManagerAccounts.length);
         // ... and it should be account "3"
         assertEquals(getTestAccountEmailAddress("3"), accountManagerAccounts[0].name);