Follow

RSOfflinePlayer - Developer Tutorial

Avatar

RSOfflinePlayer - Developer Tutorial


The RSOfflinePlayer is an iOS and Android SDK designed to allow playback of SCORM content from SCORM Engine on your mobile device. It allows your native mobile app to playback and sync SCORM content with a specially designed player package. When you are offline, with no connection, the results are saved locally. When you are online, and can connect to the SCORM Engine server, your results are synced back and appear just like other desktop browser attempts in the reporting.


The SDK uses the exact same ExternalPackageId, ExternalRegistrationId and ExternalConfiguration concepts from the SCORM Engine integration. These values are provided in the default OfflineCatalog data and are what you will use to present the available offline courses for a user through a listview of some kind in your host application.  These values are maintained locally by the SDK in the datastore and expected with each communication back to the SCORM Engine from the SDK. 


Getting Started


iOS:

Add RSOfflinePlayer-combined.a,  the RSOfflinePlayer header files and the RSOfflinePlayerResources.bundle file to your Xcode project and make sure they are linked and exported properly in your project target.


Android:

Add RSOfflinePlayer.jar to your project and make sure it is linked in your IDE.

Authentication


Authentication for the SDK is handled by the host application and not the SCORM Engine. This is the most flexible solution for adding the SDK to existing applications as well as new projects. The host application will handle the user login and simply pass an authString to the SDK when required. This authString can be anything the host application needs it to be from an encoded credentials string to an OAuth token. This authString is available in the SCORM Engine integration layer’s GetOfflineCatalog override for the host system to authenticate and allow or reject. You will be able to adjust the OfflineCatalog data based upon this authString by overriding GetOfflineCatalog in your SCORM Engine integration layer. This will allow you to only display courses to a user which they should be allowed to access.


Offline Catalog


The SCORM Engine provides a simple offline catalog that will by default include any package that has the package property of is_allowed_offline set to true. Combining this setting with the OfflineExporter service prepares a compatible course for offline playback. See the ‘Preparing Content for Offline Playback’ document for more information on this process.


RSOfflinePlayer SDK provides a getOfflineCatalogFromServer service that takes the authString and a few optional sorting and filtering options and returns a basic JSON object containing the data the host application needs to build the user’s catalog.



iOS Sample:


   [_catalog getOfflineCatalogWithConfiguration:kRSConfiguration withAuthString:_userName withLimit:[NSNumber numberWithInt:10] withOffest:[NSNumber numberWithInt:0] withSearch:@"" withCompletionBlock:^(NSString *catalogString){

       

       NSMutableArray *catalogArray = [[NSMutableArray alloc] init];

       NSError* error;

       NSDictionary *dictFromJson = [NSJSONSerialization JSONObjectWithData:[catalogString dataUsingEncoding:NSStringEncodingConversionAllowLossy] options:kNilOptions error:&error];


       NSArray *catalogFromServer = [dictFromJson objectForKey:@"offlineCatalog"];

       //get the local packages and add the data to the catalogDict

       NSArray *localPackages = [_catalog getPackageList];

       [catalogFromServer enumerateObjectsUsingBlock:^(id catObj, NSUInteger catIdx, BOOL *catStop){

           __block BOOL _userHasPackage = NO;

           [localPackages enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){

               OfflinePackage *currentPkg = (OfflinePackage *)obj;

               if([currentPkg.packageId isEqualToString:[catObj valueForKey:@"packageId"]])

               {

                   //user has this package

                   NSMutableDictionary *catalogItem = [(NSDictionary *)catObj mutableCopy];

                   [catalogItem setValue:currentPkg.packageSize forKey:@"packageSize"];

                   [catalogItem setValue:NO forKey:@"isDownloading"];

// get the current status for each local package to show the user

                   [catalogItem setValue:[RSOfflinePlayer getCurrentStatusForUser:_userName withRegistrationId:[catObj valueForKey:@"registrationId"] withConfiguration:[catObj valueForKey:@"configuration"]] forKey:@"attemptDetail"];

// add it to the catalogArray

                   [catalogArray addObject:[catalogItem copy]];

                   _userHasPackage = YES;

                   stop = YES;

               }

           }];

           //add the ones the user does not already have

           if (!_userHasPackage) {

               NSMutableDictionary *catalogItem = [(NSDictionary *)catObj mutableCopy];

               [catalogItem setValue:NO forKey:@"isDownloading"];

               [catalogArray addObject:[catalogItem copy]];

           }

       }];

       

       _offlineCatalog = catalogArray;

       [self.tableView reloadData];

       

   }withErrorBlock:^(NSError *error){

       NSLog(@"%@", [error userInfo]);

   }];






Android Sample:


  RSOfflineCatalog catalog = new RSOfflineCatalog(this,remoteServer,remotePathsForInit);

       final List<String> catalogList = new ArrayList<String>();

       catalog.getOfflineCatalogWithConfiguration("&",userName,10,0,null,new RSOfflineCatalog.getOfflineInterface() {

           @Override

           public void completionBlock(String s) {

               try{

                   JSONObject jObject = new JSONObject(s);

                   JSONArray statementArray = jObject.getJSONArray("offlineCatalog");

                   for (int i=0; i < statementArray.length(); i++)

                   {

                       try {

                           JSONObject catalogItem = statementArray.getJSONObject(i);

                           // Pulling items from the array

                           String catalogItemTitle = catalogItem.getString("title");

                           catalogList.add(catalogItemTitle);

                       } catch (JSONException e) {

                           // handle error

                       }

                   }

// update the listview here using catalogList for the adaptor

               }catch (Exception ex) {

//handle exception

               }

           }


           @Override

           public void errorBlock(String s) {

               Log.d("error", "error : " + s);

           }

       });



Playing Content and Syncing Results


To play the offline package, a RSOfflinePlayer object needs to be created by supplying a valid SCORM Engine ExternalPackageId, ExternalRegistrationId and ExternalConfiguration to the service along with the remote SCORM Engine URL and paths.


iOS Sample:


   rsop = [[RSOfflinePlayer alloc] initWithPackageId:_packageId withRegistrationId:_registrationId withConfiguration:_configuration withUserId:@"" withRemoteServer:[RSUtil getValueFromConfig:@"remote-server"] withRemotePaths:remotePaths withCompletionBlock:^{

       

       [rsop setRemoteServerUrl:[NSURL URLWithString:[RSUtil getValueFromConfig:@"remote-server"]]];

       

       //Create a URL object.

       NSURL *url = [NSURL fileURLWithPath:rsop.packageLaunchUrl];

       

       //URL Requst Object

       NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];

       

       _wv.delegate = self;

      

       [[NSURLCache sharedURLCache] removeAllCachedResponses];

       _wv.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

       //Load the request in the UIWebView.

      [_wv loadRequest:requestObj];

       

   } withErrorBlock:^(NSError *error){

       

       UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:@"Error!"

                                                            message:[NSString stringWithFormat:@"There was an error loading that package. \n %@", [error userInfo]]

                                                           delegate:nil

                                                  cancelButtonTitle:@"Ugh..."

                                                  otherButtonTitles:nil];

       [errorAlert show];

       

   } isOnline:[_isOnline boolValue]];


Android Sample:

 rsop.initWithPackageId(packageItem.packageId,"CourseId|1006!InstanceId|0!UserName|TestUser","","12",kRemoteServerUrl,remotePathsForInit,isOnline,new RSOfflinePlayer.initPackageInterface(){
           @Override
           public void completionBlock() {
               Log.d("completion Block", "the url = " + rsop.packageLaunchUrl);
               WebView webView = (WebView) findViewById(R.id.myWebView);
               webView.getSettings().setJavaScriptEnabled(true);
               webView.getSettings().setSupportMultipleWindows(true);
               webView.getSettings().setDomStorageEnabled(true);


               MyJavaScriptInterface javaInterface = new MyJavaScriptInterface();
               webView.addJavascriptInterface(javaInterface, "injectedObject");


               webView.setWebChromeClient(new WebChromeClient() {
                   public void onProgressChanged(WebView view, int progress) {
                   }
                   @Override
                   public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture, android.os.Message resultMsg)
                   {
                       RelativeLayout contentContainer = (RelativeLayout) findViewById(R.id.contentContainer);
                       contentContainer.removeAllViews();


                       WebView childView = new WebView(mContext);
                       childView.getSettings().setJavaScriptEnabled(true);
                       childView.setWebChromeClient(this);
                       childView.setWebViewClient(new WebViewClient());
                       childView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));


                       contentContainer.addView(childView);
                       WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
                       transport.setWebView(childView);
                       resultMsg.sendToTarget();
                       return true;
                   }
               });


               webView.setWebViewClient(new WebViewClient() {
                   public void onPageStarted(WebView view, String url, Bitmap favicon) {
                       super.onPageStarted(view, url, favicon);
                       Log.d("onPageStarted","url is = " + url);
                   }
                   public void onPageFinished(WebView view, String url) {
                       super.onPageFinished(view, url);
                       Log.d("onPageFinished","url is = " + url);
                       if(url.contains("results.html"))
                       {
                           view.loadUrl("javascript:( function () { var resultSrc = getRuntimeXml(); window.injectedObject.getRuntimeXml(resultSrc); } ) ()");
                           view.loadUrl("javascript:( function () { var resultSrc = getRegistrationJson(); window.injectedObject.getRegistrationJson(resultSrc); } ) ()");
                       }
                   }
                   public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
                       super.onReceivedError(view, errorCode, description, failingUrl);
                       // Do something here
                   }
               });


               webView.loadUrl("file:///" + rsop.packageLaunchUrl);
           }


           @Override
           public void errorBlock()
           {
               Log.d("failure Block", "error");
           }
       });


The SDK relies on the host application to create a UIWebView and navigate to the rsop.packageLaunchUrl as the start page. When the host application wants to sync the course data, it grabs the current values from the UIWebView’s content and sends it to the SDK for syncing. The host application supplies the isOnline value to allow various definitions of ‘online’ to be determined by the host application.


iOS Sample:


- (IBAction)updateData {

//get the current runtimeXML from the player

   NSString *runtimeXml = [_wv stringByEvaluatingJavaScriptFromString:@"Control.GetXmlForDirtyData();"];

//get the current Javascript object from the player

   NSString *attemptInfo = [_wv stringByEvaluatingJavaScriptFromString:@"GetCurrentRegistration();"];

//send both bits of data to the SDK

   [rsop syncCurrentSessionWithAttemptInfo:(NSString*)attemptInfo WithRuntimeXml:runtimeXml withOptions:nil withCompletionBlock:^{

       

   } withErrorBlock:^(NSError *error){} isOnline:[_isOnline boolValue]];  

}



Android Sample:


   class MyJavaScriptInterface {
       public void getRuntimeXml(String jsResult) {
           runtimeXml = jsResult;
       }
       public void getRegistrationJson(String jsResult) {
           attemptInfo = jsResult;
           rsop.syncCurrentSessionWithAttemptInfo(attemptInfo,runtimeXml,null,isOnline,new RSOfflinePlayer.syncCurrentSessionInterface(){
               @Override
               public void completionBlock()
               {
                   Log.d("syncCurrentSessionWithAttemptInfo completion Block", "we're done");
               }
               @Override
               public void errorBlock()
               {
                   Log.d("syncCurrentSessionWithAttemptInfo errorBlock", "sad face");
               }
           });
       }
   }




Getting Current Status on Device


To get the current SCORM status for a registration, you can use the RSOfflinePlayer’s getCurrentStatus service. If you are offline, the most recent results from the local datastore are returned. If you are online, the data will be pulled from the SCORM Engine server and then checked against the latest local results to ensure the most recent or completed attempt is used.


iOS Sample:


[RSOfflinePlayer getCurrentStatusForUser:_userName withRegistrationId:[catObj valueForKey:@"registrationId"] withConfiguration:[catObj valueForKey:@"configuration"]] forKey:@"attemptDetail"];


Android Sample:


rsop.getCurrentStatusWithCompletionBlock(new RSOfflinePlayer.getCurrentStatusInterface() {
           @Override
           public void completionBlock(Map<String, Object> stringObjectMap) {
               //display status
           }


           @Override
           public void errorBlock() {
               //handle error
           }
});


Deleting Local Content


The host application can use the SDK to delete content from the device when the user is finished with it and synced the results back to the SCORM Engine. Using RSOfflinePlayer’s deletePackage service will delete the package folder from the local filesystem as well as all of the session information in the datastore.


Special measures should be taken by the host application to not allow this for multi-user setups where once user could delete the package while another is still using it.


iOS Sample:


[_catalog deletePackageById:_packageId withCompletionBlock:^{

           NSLog(@"package deleted");

       } withErrorBlock:^(NSString *errorCode){

           NSLog(@"package delete error %@", errorCode);

       }];


Android Sample:


catalog.deletePackageWithCompletionBlock(itemToDelete, new RSOfflineCatalog.deletePackageInterface() {

           @Override

           public void completionBlock() {

               Log.d("deletePackage", "completionBlock");

           }

       });

 

Getting The SDK 

The OfflinePlayer SDKs for SCORM Engine are available only as a licensed add-on for the SCORM Engine server. Please contact us at info@scorm.com to learn more about how to license the add-on. Due to the requirement of the SCORM Engine server installation, there is no demo version of the OfflinePlayer SDK available, but please contact us to discuss your needs and we may be able to help.

Was this article helpful?
2 out of 2 found this helpful
Have more questions? Submit a request

Comments

  • Avatar
    Murtaza Ashraf

    Thanks from where I can download RSOfflinePlayer.jar.

  • Avatar
    Joe Donnelly

    Thank you for your interest Murtaza...I just responded to your ticket.  Let us know if you have any other questions there.

    Joe

  • Avatar
    muhammed ayaz

    Thanks, from where I can download RSOfflinePlayer-combined.a and Sample code App for iOS development.

  • Avatar
    Murtaza Ashraf

    Hey, Muhammad Ayaz I can help you in this regard do mail me at murtaza.ashraf@seecs.edu.pk , they have mailed me all details !

  • Avatar
    Joe Donnelly

    Hello Muhammed, 

    We are also going to be responding to your ticket her shortly...

    Thank you,

    Joe

  • Avatar
    Vaijayanthi Mala

    where I can download RSOfflinePlayer-combined.a and Sample code App for iOS development. Thanks in advance

  • Avatar
    Joe Donnelly

    Hello  Vaijayanthi

    Thank you for your message. We do not offer our Offline SDK as a free download off the site unfortunately...if you are interested in hearing more about it, I can get you in touch with one of our business folks if you like! Let me know if you want to go down that path.  Please feel free to shoot me a message at support@scorm.com.

    Thank you,

    Joe

  • Avatar
    Akib Momin
    Thanks, from where I can download RSOfflinePlayer.jar file. Please give me suggestions at akibmomin1992@gmail.com
  • Avatar
    Ryan Donnelly
    Hello Akib- Thank you for your message. Unfortunately, we're not able to provide that unless you have an active license with us. If you are interested in hearing more about it, I can get you in touch with one of our business folks if you like! Let me know if you want to go down that path. Please feel free to shoot me a message at support@rusticisoftware.com. Thanks! Ryan
  • Avatar
    Nagaraju L J

    Hi Thanks for the article, can you please help us to download and use the RSOfflinePlayer.jar, We are about to implementing e-learning in mobile (Android and iOS) and is there any other .jar for only online support. Kindly send the details to excelsarasmobileteam@gmail.com

  • Avatar
    Ryan Donnelly

    Hello Nagaraju-

    Thank you for your message. Unfortunately, we're not able to provide that unless you have an active license with us. If you are interested in hearing more about it, I can get you in touch with one of our business folks if you like! Let me know if you want to go down that path. Please feel free to shoot me a message at support@rusticisoftware.com. Thanks! Ryan

Powered by Zendesk