Sunday, April 29, 2012

Synchronization SharePoint 2010 Events Calendar with Android Calendar


Introduction:
This simple application import events from SharePoint site and then add them to Android calendar frequently by using android Service in android mobile.
      So
  • Read Events from SharePoint site by using Web service (/_vti_bin/Lists.asmx)
  •  Service run in android mobile import these events
  • Then add these events to Android calendar
  • Notify the user by Notification activity

Tools & IDEs:
  •  Android SDK 2.3 (10 level)
  • Eclipse
  • My mobile (Samsung Ace)
  • U2U CAML Query Builder
  • SharePoint 2010 Site with Calendar list

The beginning start from this activity after uploading .apk file and install it in Android mobile.



 Mail.xml (main Activity)
This activity collect the settings that the background service will use them to collect the data and these are the settings:
  • SharePoint Service URL
    • To get data from SharePoint for Non-Microsoft project or programming language is to use SharePoint Web service which is (/_vti_bin/Lists.asmx). In my case I used (GetListItems) which return items from SharePoint list by passing CAML query or view.
  • Row Limit
    • Number of rows that (GetListItems) will return
  • Option
    •  If this first time the application will import events from SharePoint so then option 1 (Get all Events) will be selected
    •  If this is not first time then option 2 (Get Last Events) will be selected. It will import all events after the last time imported ( last import time mention in green color under option 2)
  • Button
    •  Start Service to start the android service ( as windows service in .net application) to run and collect data from SharePoint Calendar list and then save them to Android calendar. This service has a timer which makes it to run daily.
    • Stop Service to stop the service and its timer



<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    
    <TextView
        android:text="@string/lblServiceUrl"
        android:layout_height="wrap_content"
        android:layout_width="fill_parent" />
    
    <EditText 
        android:id="@+id/txtServiceUrl"
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:hint="@string/lblServiceUrl" />
    
    <TextView
        android:text="@string/lblRowLimit"
        android:layout_height="wrap_content"
        android:layout_width="fill_parent" />
    
    <EditText 
        android:id="@+id/txtRowLimit"
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:inputType="number"
        android:maxLength="3" />
    
    <TextView
        android:text="@string/lblOptions"
        android:layout_height="wrap_content"
        android:layout_width="fill_parent" />
    
    <RadioGroup 
        android:id="@+id/rdbGpOptions"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >
            <RadioButton 
                android:id="@+id/rdb1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/lblGetAllEvents"
                android:enabled="false" />
            <RadioButton 
                android:id="@+id/rdb2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/lblGetLastMonthEvents"
                android:checked="true"
                android:enabled="false" />
    </RadioGroup>
    
    <TextView 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textColor="#00ff00"
        android:id="@+id/lblLastUpdate"
        android:visibility="gone" />

    <Button 
        android:id="@+id/btnStartService"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/lblStartService" />
    
    <Button 
        android:id="@+id/btnStopService"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/lblStopService" />

</LinearLayout>


package sqlgoogler.blogspot.com;

import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;


public class SyncSharePointCalendarActivity extends Activity {
   
    private EditText txtServiceUrl ,txtRowLimit;
    private Button btnStartService , btnStopService;
    private RadioButton rdb1 , rdb2;
    private TextView lblLastUpdate;
    private SharedPreferences settings;
    private String lastUpdate;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        //Get settings from Shared Preferences 
        settings = getSharedPreferences(Constants.Pref_Name, 0); 
        
        txtServiceUrl = (EditText)findViewById(R.id.txtServiceUrl);
        txtRowLimit = (EditText)findViewById(R.id.txtRowLimit);
        lblLastUpdate = (TextView)findViewById(R.id.lblLastUpdate);
        
        rdb1 = (RadioButton)findViewById(R.id.rdb1);
        rdb2 = (RadioButton)findViewById(R.id.rdb2);
   
        try
        {
            txtServiceUrl.setText(settings.getString(Constants.Service_URL_KEY, Constants.Service_URL_VALUE));
            txtRowLimit.setText(settings.getString(Constants.Row_Limit_KEY, Constants.Row_Limit_VALUE));
            lastUpdate = settings.getString(Constants.Last_Update_KEY, Constants.Last_Update_VALUE);
            
            if(lastUpdate != "")
            {
                lblLastUpdate.setText(String.format("%s %s",getResources().getString(R.string.msgLastUpdate), lastUpdate));
                lblLastUpdate.setVisibility(View.VISIBLE);
                rdb2.setChecked(true);
                rdb1.setChecked(false);
            }
            else
            {
                lblLastUpdate.setVisibility(View.GONE);
                rdb2.setChecked(false);
                rdb1.setChecked(true);
            }
        }
        catch(Exception ex)
        {            
            txtServiceUrl.setText( Constants.Service_URL_VALUE);
            txtRowLimit.setText(Constants.Row_Limit_VALUE);
            rdb2.setChecked(true);
            rdb1.setChecked(false);
        }
        
        //Start Service Event
        btnStartService = (Button) findViewById(R.id.btnStartService);
        btnStartService.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                
                //Save settings to Shared Preferences
                SharedPreferences.Editor editor = settings.edit();
                editor.putString(Constants.Service_URL_KEY,txtServiceUrl.getText().toString().trim());
                editor.putString(Constants.Row_Limit_KEY,txtRowLimit.getText().toString().trim());
                    
                editor.commit();
                
                //Start the service
                startService(new Intent(getBaseContext(), SyncService.class));
                showMessage(getResources().getString(R.string.StartMsg));
            }     
        });
        
        //Stop Service Event
        btnStopService = (Button) findViewById(R.id.btnStopService);
        btnStopService.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                
                //Stop the service
                stopService(new Intent(getBaseContext(), SyncService.class));
                showMessage(getResources().getString(R.string.StopMsg));
            }
        });
    }
    
    private void showMessage(String msg)
    {
        Toast.makeText(getBaseContext(),msg, Toast.LENGTH_SHORT).show(); 
    } 
}

Second part of this application which did the most jobs is Android Service (SyncService):
this service did the following :

  1. Read settings from Shared Preferences (like SharePoint Service Url, Row Limit,..)
  2. This service run daily by using Timer
  3. Consume SharePoint Service and returned SOAP 
  4. parsing this SOAP and add the return data to Android Calendar
  5. Show Notification about the status of import if it's successed or failed

package sqlgoogler.blogspot.com;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.protocol.HTTP;
import android.util.Log;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.IBinder;

public class SyncService extends Service  {
    private Timer timer = new Timer(); 
    private SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 
    private long updateInterval;
    private SharedPreferences settings;
    
    private String serviceUrl;
    private String rowLimit;
    private String lastUpdate;
    
    public IBinder onBind(Intent intent) {
      return null;
    }

    @Override 
    public void onCreate() 
    {
      super.onCreate();
      
      //Read settings from SharedPreferences
      settings = getSharedPreferences(Constants.Pref_Name, 0);
      updateInterval = 1000 * 60 * 60 * 24; // By Day
      updateInterval= 5000 * updateInterval;
      
      serviceUrl = settings.getString(Constants.Service_URL_KEY, Constants.Service_URL_VALUE);
      rowLimit = settings.getString(Constants.Row_Limit_KEY, Constants.Row_Limit_VALUE);
      lastUpdate = settings.getString(Constants.Last_Update_KEY, Constants.Last_Update_VALUE);
        
      _startService();
    }

    @Override 
    public void onDestroy() 
    {
      super.onDestroy();
      _shutdownService();
    }

    private void _startService()
    {      
      timer.scheduleAtFixedRate(    
          
              new TimerTask() {

                    public void run() {

                        try{

                        doServiceWork();

                        Thread.sleep(updateInterval);

                        }catch(InterruptedException ie){
                        }      
                    }
                  },
                  Constants.Delay_Interval,
                  updateInterval);
    }
    
    private void _shutdownService()
    {
      if (timer != null) timer.cancel();
      Log.i(getClass().getSimpleName(), "Timer stopped...");
    }
    
    private void doServiceWork()
    {  
        try 
        {
            HttpClient httpclient = new DefaultHttpClient();
            HttpPost httppost = new HttpPost(serviceUrl);
            StringEntity se;
        
            if(lastUpdate != "")
                //CAML query for Last Events
                se = new StringEntity( String.format("<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><GetListItems xmlns=\"http://schemas.microsoft.com/sharepoint/soap/\"><listName>eventsCal</listName>%s<rowLimit></rowLimit><viewName></viewName><query><Query><Where><Gt><FieldRef Name='EventDate' /><Value Type='DateTime'>%sZ</Value></Gt></Where><OrderBy><FieldRef Name='Title' /></OrderBy></Query></query></GetListItems></soap:Body></soap:Envelope>", rowLimit, lastUpdate), HTTP.UTF_8);
            else
                //CAML query for All Events
                se = new StringEntity( String.format("<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><GetListItems xmlns=\"http://schemas.microsoft.com/sharepoint/soap/\"><listName>eventsCal</listName><rowLimit>%s</rowLimit><viewName></viewName><query><Query><OrderBy><FieldRef Name='Title' /></OrderBy></Query></query></GetListItems></soap:Body></soap:Envelope>", rowLimit), HTTP.UTF_8);    
            se.setContentType("text/xml");
            httppost.setEntity(se);

            HttpResponse httpresponse = httpclient.execute(httppost);
            InputStream in = httpresponse.getEntity().getContent();
            String str = inputStreamToString(in).toString();
            
            readSoap(str);
            
            //Show Notification 
            displayNotification(String.format("%s",getResources().getString(R.string.msgImported)));
            
            //Set Last Update setting 
            SharedPreferences.Editor editor = settings.edit();
            editor.putString(Constants.Last_Update_KEY, df.format(new java.util.Date()).toString());
                
            editor.commit();
            
        } catch (ClientProtocolException e) {
            displayNotification(String.format("%s \n Error Details : %s",getResources().getString(R.string.msgImportedFailed),e.toString()));
        } catch (IOException e) {
            displayNotification(String.format("%s \n Error Details : %s",getResources().getString(R.string.msgImportedFailed),e.toString()));
        }
    }
    
    private void readSoap(String in)
    {    
        Document doc = null;
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        try 
        {
            DocumentBuilder db = dbf.newDocumentBuilder();
            InputSource is = new InputSource();
            is.setCharacterStream(new StringReader(in));
            doc = db.parse(is); 

            } catch (ParserConfigurationException e) {
            } catch (SAXException e) {
            } catch (IOException e) {
            }
            
            //---retrieve all the <z:row> nodes---
            NodeList elements = doc.getElementsByTagName("z:row"); 
            
            for (int i = 0; i < elements.getLength(); i++) { 
                Node itemNode = elements.item(i); 
               
                if (itemNode.getNodeType() == Node.ELEMENT_NODE) 
                {            
                    //---convert the Node into an Element---
                    Element element = (Element) itemNode;

                    addEvent(element.getAttribute("ows_Title"), element.getAttribute("ows_EventDate"), element.getAttribute("ows_EndDate"), element.getAttribute("ows_Description"), element.getAttribute("ows_Location"));
                } 
            } 
    }
    
    private void addEvent(String title , String startEvent , String endEvent, String desc , String location) 
    {
        try{
            // date
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date date1 = new Date();
            Date date2 = new Date();
            try {
                 date1 = format.parse(startEvent);
                 date2 = format.parse(endEvent);
            } catch (ParseException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
                       
            Calendar cal = Calendar.getInstance();
            cal.setTime(date1);
            long startTime = cal.getTimeInMillis() + 1000;
            cal.setTime(date2);
            long endTime   = cal.getTimeInMillis() + 1000;
            
            ContentValues event = new ContentValues();
            event.put("calendar_id", getCalendar_ID());
            event.put("title", title);
            event.put("description", desc);
            event.put("eventLocation", location);           
            event.put("dtstart", startTime);
            event.put("dtend", endTime);
            event.put("allDay", 0);
            getContentResolver().insert(Uri.parse("content://com.android.calendar/events"), event); 
          
        }catch(Exception ee){}
    }
    
    private int getCalendar_ID() {
        int calendar_id         = 0;
        String[] projection     = new String[] { "_id", "name" };
        String selection        = "selected=1";
        String path             = "calendars";
        Cursor calendarCursor   = getCalendarCursor(projection, selection, path);

        if (calendarCursor != null && calendarCursor.moveToFirst()) {
            int nameColumn = calendarCursor.getColumnIndex("name");
            int idColumn   = calendarCursor.getColumnIndex("_id");
            do {
                String calName  = calendarCursor.getString(nameColumn);
                String calId    = calendarCursor.getString(idColumn);
                if (calName != null) {
                    calendar_id = Integer.parseInt(calId);
                }
            } while (calendarCursor.moveToNext());
        }
        return calendar_id;
    }
    
    private Cursor getCalendarCursor(String[] projection, String selection, String path) {
        Uri calendars = Uri.parse("content://calendar/" + path);
        Cursor cCursor = null;
        try 
        {
            cCursor = getContentResolver().query(calendars, projection, selection, null, null);
        } 
        catch (IllegalArgumentException e) {}
        if (cCursor == null) 
        {
            calendars = Uri.parse("content://com.android.calendar/" + path);
            try 
            {
               cCursor = getContentResolver().query(calendars, projection, selection, null, null);
            } 
            catch (IllegalArgumentException e) {}
        }
        return cCursor;
    }
          
    private StringBuilder inputStreamToString(InputStream is) 
    {
        String line = "";
        StringBuilder total = new StringBuilder();
        // Wrap a BufferedReader around the InputStream
        BufferedReader rd = new BufferedReader(new InputStreamReader(is));
        // Read response until the end
        try 
        {
         while ((line = rd.readLine()) != null) 
         { 
           total.append(line); 
         }
        } catch (IOException e) {
         e.printStackTrace();
        }
        
        // Return full string
        return total;
     }

    protected void displayNotification(String msg)
    {
        Intent i = new Intent(this, NotificationActivity.class);
        i.putExtra("notificationID", 1);
        i.putExtra("msg", msg);
        PendingIntent pendingIntent =
            PendingIntent.getActivity(this, 0, i, 0);
        NotificationManager nm = (NotificationManager)
        getSystemService(NOTIFICATION_SERVICE);
        Notification notif = new Notification(
        R.drawable.ic_launcher,getResources().getString(R.string.notificationdesc), System.currentTimeMillis());
        CharSequence from = getResources().getString(R.string.notificationtitle);
        CharSequence message = getResources().getString(R.string.notificationdesc);
        notif.setLatestEventInfo(this, from, message, pendingIntent);
        //---100ms delay, vibrate for 250ms, pause for 100 ms and
        // then vibrate for 500ms---
        notif.vibrate = new long[] { 100, 250, 100, 500};
        nm.notify(1, notif);
    }
}


so first thing this service will consume the SharePoint service and get items form events list

you will notice in the below code we built a String entity which contains the name of list and method name (GetListItems) and CAML Query


HttpClient httpclient = new DefaultHttpClient();
            HttpPost httppost = new HttpPost(serviceUrl);
            StringEntity se;
        
            if(lastUpdate != "")
                //CAML query for Last Events
                se = new StringEntity( String.format("<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><GetListItems xmlns=\"http://schemas.microsoft.com/sharepoint/soap/\"><listName>eventsCal</listName>%s<rowLimit></rowLimit><viewName></viewName><query><Query><Where><Gt><FieldRef Name='EventDate' /><Value Type='DateTime'>%sZ</Value></Gt></Where><OrderBy><FieldRef Name='Title' /></OrderBy></Query></query></GetListItems></soap:Body></soap:Envelope>", rowLimit, lastUpdate), HTTP.UTF_8);
            else
                //CAML query for All Events
                se = new StringEntity( String.format("<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><GetListItems xmlns=\"http://schemas.microsoft.com/sharepoint/soap/\"><listName>eventsCal</listName><rowLimit>%s</rowLimit><viewName></viewName><query><Query><OrderBy><FieldRef Name='Title' /></OrderBy></Query></query></GetListItems></soap:Body></soap:Envelope>", rowLimit), HTTP.UTF_8);    
            se.setContentType("text/xml");
            httppost.setEntity(se);

            HttpResponse httpresponse = httpclient.execute(httppost);
            InputStream in = httpresponse.getEntity().getContent();
            String str = inputStreamToString(in).toString();

so then we are parsing the return XML or SOAP and add these retrun items or events to Android Calendar.


Notes:

  • I have tested this application in my Android mobile (Samsung Ace) with version 2.3 (level 10)
  • this sample as starter and need more enchantments and test
  • you can't test add event calendar to android using emulator because does not has calendar application
  • You can download the code from here

2 comments:

Mohammad Fawaz said...

Good job and idea Man...

Prasanna said...

really great job...
hats off to you