MS Word Basics: Outlining and TOC

This might seem like a relatively non-technical topic compared with the other posts on my blog, but I suspect these tips will be useful to somebody!  One of my reasons for keeping a blog is to record instructions for processes that I’ve had to explain more than once, and there are a few MS Word tricks which can save you a lot of time.

Images are from Word 2013, but the processes are similar with any version of Word.  Click on them to see a larger version.

Use the Built-in Styles

You need to use Header 1, Header 2 etc., when creating titles for your document.   These techniques will not work otherwise.  If you need more convincing, here’s a thorough post on the subject.

One great thing about the built in styles is that MS Word has shortcuts for them:
Control-Alt-1 applies heading 1, etc.   This makes reorganization of your document incredibly easy.

Creating an “Outline Document”

You might want to number the sections of your document to look like an outline. For example:
Numbered Headers
This is most easily done by assigning numbering to your headers, and using them as titles for the sections of your document.  This approach also plays well with the Table of Contents, also covered below.

  1. Click the Multilevel List button on the Home Tab.
    Multilevel List button
  2. Here’s a nice trick (optonal):  Select the list library which looks most similar to the outline that you want.  That will be your starting point.  You’ll have to click the Multilevel List button again.
  3. At the bottom of this dialog, select Define a new Multilevel List
    Define a Multilevel List
  4. Click on the More button to see the entire Define new Mutlilevel List dialog
  5. Let’s say you want a two-level outline.
    In the left of the dialog where you see Click level to modify, click on 1.  On the right where it says Link level to style, click on Heading 1.    Do the same for level 2.  
    Define a Multilevel List
  6. Click OK.  Word will automatically modify the appropriate styles.

Don’t like it?  Control-Z will undo the whole thing!

Table of Contents

  1. First position your cursor at the beginning of the Document, after the title page, or wherever you want your TOC to be.
  2. You will probably want the table on its own page.  This is also important because the table itself takes up room.  If so, Control-Enter will give you a page break.
  3. The trickiest part of this is finding the button – look on the References Tab:TOC button
  4. Once the table is in your document, you will need to tell it when to update.  Just click on it and and an Update Table button will pop up in the upper left. Update Table Button
  5. (Optional) You can also customize your table of contents, applying styles or telling Word how many levels to show.  Click the Table of Contents button and select Custom Table of Contents from the dropdown.

Help, I messed up my styles!

Let’s say you’ve changed your header styles and want to go revert to the Word default.  Use the following process to locate the Normal Template file, which you can then delete or rename.  MS Word will recreate it when restarting. Note that you will lose other custom settings too, like keyboard shortcuts.  Fortunately you can always go back, using creative file naming!

  1. Shut down MS Word.
  2. Open a file explorer (or “My Computer”) and paste the following into the window at the top: %appdata%\Microsoft\Templates, and hit return.
  3. Find the file Normal.dotm and rename or delete it.
  4. Restart MS Word.

Securely Transmitting Information Across AngularJS to .NET Web API Services

Working with angularJS and .NET WebAPI 2 presents some challenges. In this scenario, the client side of our app is a single-page rich java client application written in AngularJS, and makes heavy use of web services. The web services are written in WebApi. Ultimately this app will be a SharePoint 2013 provider hosted app (and appear in an IFrame in SharePoint).

Here’s the issue: Our internal data retrieval process requires user information (i.e. Login ID), but the web services (although the are protected by forms authentication) are not aware of WHO the user is. We need a way to securely pass the user ID whenever a web service request is made.

You can do this by saving user information to a client side cookie. Information is encrypted, so it’s not possible for one user to impersonate another. The encryption key is located on the server.

Note: AES 256 bit Encryption code is needed to implement this. I got mine from this excellent article.

Authenticating architecture

The logic works as follows:
Initial page load:

  1. Step A: Login info is detected, and user information is determined (ASP Page codebehind).
  2. Step B: User information is encrypted and saved to a client-side cookie (ASP Page codebehind).

Web service request:

  1. Step C: AngularJS picks up cookie and passes it in request header.
  2. Step D: On server side, the user token is picked up from the header and validated (using a FilterAttribute). note: so it applies to all.
  3. Step E: User token is decrypted, and can be used internally for data retrieval etc.

Steps A and B: User Authentication and Encryption

When the initial page loads, you’ll want to know if the user is logged in. The default app (when setting up in Visual Studio) comes with the wiring necessary to determine the SharePoint user. In my case there’s a third-party authentication provider, but the logic here is the same.

public clientSideUserToken;

protected void Page_Load(object sender, EventArgs e)
{
	bool isLoggedIn = false;
	string userName = null;

	//Authentication Provider logic for determining login.
	//SharePoint logic illustrated here.
	var spContext = SharePointContextProvider.Current.GetSharePointContext(Context);
	if (spContext != null)
	{
		using (var clientContext = spContext.CreateUserClientContextForSPHost())
		{
			if (clientContext != null)
			{
				Microsoft.SharePoint.Client.User spUser = clientContext.Web.CurrentUser;
				clientContext.Load(spUser, user => user.Title);
				clientContext.ExecuteQuery();
				isLoggedIn = true;
				userName = spUser.Title;
			}
		}
	}
	
	if (isLoggedIn)
	{
		string token = EncryptUsername(Session, userName);

                //drop cookie for angular to pick up. 
                string cookieName = "MYCOOKIE";
                Response.Cookies[cookieName]["USERTOKEN"] = token;
                Response.Cookies[cookieName].Expires = DateTime.Now.AddMinutes(1);
	}
	else
	{
		//HANDLE USER NOT LOGGED IN.
	}
}

private string EncryptUsername(HttpSessionState session, string userName)
{
    byte[] byUserName = Encoding.ASCII.GetBytes(userName);
    byte[] byEncryptedUserName = AESEncryption.AES_Encrypt(byUserName, Config.CookieEncryptionKey);
    string encryptedUserName = Convert.ToBase64String(byEncryptedUserName);
    return encryptedUserName;
}

Step C: AngularJS Submits User Information Header to Services

In this step, AnglularJS picks up the cookie dropped by the server on the initial page load (see step B), and puts it into the header of the next request.

This code is best located in an AngluarJS Interceptor (read more about interceptors here). The following ‘request’ function will execute every time the user makes a service request.

MyModule.factory('serviceLogger', function ($q, $rootScope, serviceErrorHandler, globalData, $cookies, $cookieStore) {
    return {
        'request': function (config) {
            //pick up the asp cookie if present, pull out the encrypted user token, and put it into the header.
            var aspcookie = $cookies["MYCOOKIE"];
            if (aspcookie) {
                var jsonCookie = {};
                var items = aspcookie.split("&");
                for (var i = 0; i < items.length; i++) {
                   var splitPos = items[i].indexOf('=');
                   jsonCookie[items[i].substring(0, splitPos)] = items[i].substring(splitPos + 1);
                }
                config.headers.HEADERTOKEN = jsonCookie.USERTOKEN;
            }

            return config;
        },
    };

Steps D and E: Add a Custom Authentication Filter into WebApi to Check for Valid User

Finally, the header from step C is decoded and validated. If validation fails, the user gets a HTTP 401 (forbidden) error.

Using WebAPI, you can write code which can intercept all requests. To do this, write a custom authenticator implementing IAuthenticationFilter. Then apply it to ALL web services using the following code in your Global.asax:

protected void Application_Start(object sender, EventArgs e)
{
    //whatever else you've got here
    GlobalConfiguration.Configuration.Filters.Add(new ValidateUserToken());
}

IMPLEMENTATION NOTE: tO implement the IAuthenticationFilter, I used an abstract filter to do some of the heavy lifting. I found a great example of that here. Another great help was this blog, which provides a straightforward way to implment IHttpActionResult. This implementation even allows us to send a custom object back to the client, along with the appropriate HTTP code!

Here’s an example of that class:

public class ValidateUserToken : AAuthenticationFilterAttribute
{
    public override void OnAuthentication(System.Web.Http.Filters.HttpAuthenticationContext context)
    {
            try
            {
                DecryptUsername(context.Request);
            }
            catch
            {
                AuthResult msg = new AuthResult() { Message = "Server rejected the request.  Authorization invalid or expired." };
                context.ErrorResult = new SimpleHttpResult<AuthResult>(context.Request, HttpStatusCode.Forbidden, msg);
            }
            //otherwise pass through and everything is ok. 
    }

    //note (step E): if you make this method static, you can retrieve the user name at will.
    private static string DecryptUsername(HttpRequestMessage request)
    {
        string encryptedUserName = request.Headers.GetValues("HEADERTOKEN").First();
        byte[] byEncryptedUserName = Convert.FromBase64String(encryptedUserName);
        byte[] byUserName = AESEncryption.AES_Decrypt(byEncryptedUserName, Config.CookieEncryptionKey);
        string userName = Encoding.ASCII.GetString(byUserName);
        return userName;
    }
}

//Very simple result object.   Any properties added here will be passed to the client side, as JSON.
public class AuthResult
{
    public string Message;
}

Once these methods are in place, no further code modifications are necessary, and we can now write services and use them with confidence.

SharePoint App Part Resizing, When Using AngularJS

Provider Hosted Apps

Sometimes it’s necessary to embed rich, non-SharePoint functionality in a SharePoint site. The preferred way to do this in SharePoint 2013 is to use a Provider Hosted App. SharePoint then acts as a “host” for the app, which can function independently. This gives the ability to layer in complex business rules, perform development cycles AWAY from SharePoint, and then embed the functionality in the site.

IFrame Presentation

Taking this route, one challenge is to ensure that the app appears as part of the page. Provider Hosted Apps are hosted in an iframe, but users need not be aware of this fact. If not done properly, parts of the app might disappear or for scroll bars may appear in the frame. Generally the iframe content cannot control the size of the “parent” window.

So it’s up to the developer to ensure that the iframe is sized properly for the app. Fortunately, Microsoft has anticipated this issue and given us some javascript “hooks” to do this. There are a number of blogs on how to achieve this, from simple implementation to complex ones . Bottom line, your hosted javascript code needs to send a message similar to this one to the parent window:

<message senderId=12345ABCD>resize(100%,800px)</message>

Additional Challenges with AngularJS

Resizing the parent iFrame is a simpler task with a “traditional” (mostly server side) app – then page size changes when the page reloads, and the necessary logic can be run then.

But with AngularJS (a single page app), the iframe needs to resize when we move from one “view” to another, or the page size changes due to the underlying javascript logic. When resizing, we need to know what the size of our content will be. Fortunately, this is not rocket science – it just requires a “harmonic convergence” of code.

Step 1: Add wrapper HTML

Identify or create a wrapper div which will contain all HTML in your app. We will check this wrapper to determine how large the iframe should be.

<body ng-app="appName">
    <div main-wrapper>
        <!-- Add your site or application content here -->
        <div ng-view=""></div>
    <div>
</body>

Step 2: Add Resizing Code

Add some javascript code to resize the iframe. The following code assumes that only the height of your app will vary. Note the chrome fix – this is another issue that applies only to Angular implementations.

"use strict";
function adjustFrameSize(contentHeight) {
    var senderId,
        resizeMessage = '<message senderId={Sender_ID}>resize({Width}, {Height}px)</message>';

    var args = document.URL.split("?");
    if (args.length < 2) return;
    var params = args[1].split("&");
    for (var i = 0; i < params.length; i = i + 1) {
        var param = params[i].split("=");
        if (param[0].toLowerCase() == "senderid") {
            senderId = decodeURIComponent(param[1]);
            senderId = senderId.split("#")[0]; //for chrome - strip out #/viewname if present
        }
    }

    var step = 30, finalHeight;
    finalHeight = (step - (contentHeight % step)) + contentHeight;

    resizeMessage = resizeMessage.replace("{Sender_ID}", senderId);
    resizeMessage = resizeMessage.replace("{Height}", finalHeight);
    resizeMessage = resizeMessage.replace("{Width}", "100%");
    console.log(resizeMessage);
    window.parent.postMessage(resizeMessage, "*");
}

Step 3: Add an Angular Directive

In your AngularJS code, create a directive which will attach to your wrapper div. What you’re doing here is creating an event which can be fired at any time from your app, to trigger a resize of the iframe based on the height of your wrapper div.

  .directive('mainWrapper', function ($timeout) {
        return {
            restrict: 'A',
            link: function (s, e, attrs) {
                s.$on('resizeframe', function () {
                    $timeout(function () {
                        //timeout ensures that it's run after the DOM renders.
                        adjustFrameSize(e[0].offsetHeight);
                    }, 0, false);
                });
            }
        };
    })

Step 4: Trigger the Resizing Event

Finally, you need to instruct the event to fire at the appropriate times. This might be when a controller is intially accessed, or perhaps when data are accessed. The strength of this method is that you can control when resizing occurs. You need to inject $scope and use $broadcast. Here are a couple of examples:

.controller('FirstPageControler', function ($scope) {
      //resize frame when controller loads
      $scope.$parent.$broadcast('resizeframe');
})

.controller('AnotherControler', function ($scope, webServices) {
      webServices.reports().then(function (response) {
          $scope.reports = response;

          //resize frame upon successful return from a service.
          //presumably we have returned with data that will resize the page.
          $scope.$parent.$broadcast('resizeframe');
      }, function (response) {
          $scope.reports = response;
      });
})

That should do the job! Good luck, and feel free to leave a comment if you’ve found a better way.

NLog Database Configuration

When using NLog on web sites (most notably in SharePoint or in HttpModules), I’ve noticed some problems with file logging. So I found myself looking for a good database configuration which matches some basic needs. I settled on the following:

In the web.nlog file:

<?xml version="1.0" encoding="utf-8"?>
<!-- This section contains the NLog configuration settings -->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <targets>
    <target name="database" type="Database"> 
      <connectionString> 
        Data Source=localhost; Integrated Security=SSPI;Initial Catalog=MyDatabase 
      </connectionString> 
      <commandText> 
        insert into system_logging(log_date,log_level,log_logger,log_message, log_message_id, log_user_name, log_call_site, log_thread, log_exception, log_stacktrace) values(@time_stamp, @level, @logger, @message,@msgid, @user_name, @call_site, @threadid, @log_exception, @stacktrace); 
      </commandText> 
      <parameter name="@time_stamp" layout="${longdate}"/> 
      <parameter name="@level" layout="${level}"/> 
      <parameter name="@logger" layout="${logger}"/> 
      <parameter name="@message" layout="${message}"/> 
      <parameter name="@msgid" layout="${event-context:item=UniqueCode}"/> 
      <parameter name="@user_name" layout="${windows-identity:domain=true}"/> 
      <parameter name="@call_site" layout="${callsite:filename=true}"/>
      <parameter name="@threadid" layout="${threadid}"/> 
      <parameter name="@log_exception" layout="${exception}"/> 
      <parameter name="@stacktrace" layout="${stacktrace}"/>
    </target> 
  </targets>
 
  <rules>
    <logger name="*" minlevel="Trace" writeTo="database"/>
  </rules>
</nlog>

Here’s the script to create the necessary table:

USE [MyDatabase]
GO
/****** Object:  Table [dbo].[system_logging]    Script Date: 10/14/2014 2:06:10 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[system_logging](
	[system_logging_guid] [uniqueidentifier] ROWGUIDCOL  NOT NULL,
	[entered_date] [datetime] NULL,
	[log_application] [varchar](200) NULL,
	[log_date] [varchar](100) NULL,
	[log_level] [varchar](100) NULL,
	[log_logger] [varchar](8000) NULL,
	[log_message] [varchar](8000) NULL,
	[log_message_id] [varchar](8000) NULL,
	[log_user_name] [varchar](8000) NULL,
	[log_call_site] [varchar](8000) NULL,
	[log_thread] [varchar](100) NULL,
	[log_exception] [varchar](8000) NULL,
	[log_stacktrace] [varchar](8000) NULL,
 CONSTRAINT [PK_system_logging] PRIMARY KEY CLUSTERED 
(
	[system_logging_guid] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[system_logging] ADD  CONSTRAINT [DF_system_logging_system_logging_guid]  DEFAULT (newid()) FOR [system_logging_guid]
GO
ALTER TABLE [dbo].[system_logging] ADD  CONSTRAINT [DF_system_logging_entered_date]  DEFAULT (getdate()) FOR [entered_date]
GO

Link back to Android app using intent-filter

This post covers a simple scenario where we have a web page (for example, on our organization’s mobile site) that we would like to link back to the mobile app.

In our AndroidManifest.xml, create an intent-filter element as follows:

<intent-filter>
    <data android:scheme="ourorg" />
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.BROWSABLE" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

If you place this under the main element representing your app, then the following link will invoke your app.
Now the link ourorg:// will link back to the app.

There’s a lot more that you can do with the intent-filter. For now, we’re just covering a simple case here.

More information on intent-filter on developer.android.com

Signing Your Cordova Android App

So your app is tested, and relatively bug-free – congratulations! Now it’s time to publish on Google Play. You’ll find out pretty quickly that there are a couple of modifications you’ll need to make to your app.

Unfortunately I found that I had to do quite a bit of digging to get my release process *almost* automated. There are full instructions for this here in the Cordova docs, but I’m offering a simpler explanation that will work signing into the automation.

Version Code

You’ll need to change the android:versionCode property in your AndroidManifest.xml file. Google Play will treat your app as a new version every time this value is incremented. If anyone knows how to work this into the Cordova automation, please comment on this post – I would love to hear about it. My current thought is that Cordova Hooks should work for this, but at the moment I’m not going down this path.

<manifest android:hardwareAccelerated="true" android:versionCode="2" android:versionName="1.2" ....

Creating the Key for Signing

First, create a keystore, using the keygen tool. In the following example, I’m generating one for my fictitious client, NCA:

$ keytool -genkey -v -keystore nca.keystore -alias ncamobile -keyalg RSA -keysize 2048 -validity 10000
Enter keystore password:
Re-enter new password:
What is your first and last name?
  [Unknown]:  Michael Mendelson
What is the name of your organizational unit?
  [Unknown]:  NCA
What is the name of your organization?
  [Unknown]:  National Client of America
What is the name of your City or Locality?
  [Unknown]:  San Francisco
What is the name of your State or Province?
  [Unknown]:  CA
What is the two-letter country code for this unit?
  [Unknown]:  US
Is CN=Michael Mendelson, OU=NCA, O=National Client of America, L=San Francisco, ST=CA, C=US correct?
  [no]:  yes

Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10,000 days
        for: CN=Michael Mendelson, OU=NCA, O=National Client of America, L=San Francisco, ST=CA, C=US
Enter key password for <ncamobile>
        (RETURN if same as keystore password):
[Storing nca.keystore]

Show the Android Build Scripts Where the Keystore is

When you build in release mode using Cordova, the Android build scripts are invoked. You’ll need to add some properties to a place where the build can see them. If you’d rather not enter the passwords, you’ll be prompted during the build.

Cordova 4.0+

Cordova currently builds with Gradle. When the –release switch is used, Gradle scripts will look for a file called release-signing.properties in platforms/android. Create it, and add the following settings:

storeFile=../../nca.keystore
storeType=jks
keyAlias=ncamobile
keyPassword=passwordhere
storePassword=passwordhere

There are a few other ways to do this. There’s a great StackOverflow reference for that. There’s also more detail here in the Cordova docs

Older Cordova

Properties need to be in a place where Ant can find them. I added them to my platforms/android/local.properties file.

key.store=c:\\dev\\ncamobile\\nca.keystore
key.alias=ncamobile
key.store.password=passwordhere
key.alias.password=passwordhere

Run the Release Build

The following should do it:

cordova build android --release

If you build without the –release argument, the app will still be signed with the Debug Key.

One issue I’d like to overcome is that items in AndroidManifest.xml and local.properties are NOT checked into our code repository, and in fact are regenerated, modified or overwritten by various Cordova processes. While it would make sense that configuration properties like this one would be set-able into the config.xml, that does not appear to be the case. Of course that impedes our ability to just check out the code and run a release build.

I’ll look for ways to smooth out this process in the future, and any insights are welcome.

Use Cordova to Initiate an Email

Since your Cordova app runs in an HTML Layout Engine, there’s a very straight-forward way to initiate an email: a link.

However, if you want to create a nicely formatted email, or a longer one, you need to use a plugin to invoke the native interface. I went with katzer’s cordova-plugin-email-composer, and it works well.

My purpose was to simply create a blank email draft, with only title and content.

The following code wrapper encapsulates the plugin call, taking into account:

  • we’re running on a device with email not installed
  • we’re running on a non-mobile device (as I often do when debugging the app)
function draftEmail(subject, message) {
    if (!window.plugin){
        //non-mobile - plugins are not present.
        alert("Email plugin is not available");  
        return;
    }
    if (!isAvailable){
        //mobile, but no email installed
        alert("Email is not available")
        return;
    }
    window.plugin.email.open({
        to: [], cc: [], bcc: [], attachments: [],
        subject: subject,
        body: message,
        isHtml: true
    });
}

//runs on application startup, to check for installed email
var isAvailable = false;
if (window.plugin){
    window.plugin.email.isServiceAvailable(
        function (emailInstalled) {
            isAvailable = emailInstalled;
        }
    );
}

It would be more elegant to hide the “Share” button if email is not installed on the device, but for demo purposes this works.

Build Configurations in Cordova using Hooks

When working with Cordova or Phone Gap, you might need a way to configure your dev build differently from your production build. For example, you might have a set of web services to be used for development, and another for production. This simple Cordova Hook loads the configuration appropriate to the build.

Hooks are a well documented mechanism in Cordova which executes the developer’s custom scripts at certain points in the build process. The best example I have found is this post by Dan Moore.

In this example, all settings for a given configuration are in one file. The appropriate files is substituted in when you build (or “prepare”) your project. This is written in node.js for portability.

  1. Wire up your code so that all “switchable” settings are in one file, config.js. Here’s an example:
    var config = {
        oneSvc: "http://www.example.com/ws/MyService.svc/MyService",
        anotherSvc: "http://www.example.com/ws/YourService.svc/YourService"
    }
    

    …then use config.oneSvc and config.anotherSvc to access the settings in your code. Remember to include config.js in your index.html.

  2. Create an alternate config file in your config directory, called config-prod.js or config-test.js.
  3. In your development environment, set a variable called TARGET with the appropriate value (e.g. “prod” or “test”). If this variable is not set, config.js will be used as-is.
  4. Place the following code in your .cordova/hooks/after_prepare directory.
  5. Build the project as usual.
#!/usr/bin/env node
/**
 * Created by Michael on 5/13/2014.
 *
 * Detect the current build by looking at the TARGET env variable.
 * If TARGET does not exist, make no change.
 * Otherwise, replace config.js with config/config-xxxx.js (where TARGET=xxxx)
 * If config-xxxx.js doesn't exist, execution will fail.
 *
 * Before running this, set the target as:
 *    export TARGET=prod
 *
 */
var fs = require("fs");
var path = require("path");
var rootdir = process.argv[2];
console.log("Running hook: "+path.basename(process.env.CORDOVA_HOOK));

if (process.env.TARGET) {
    var srcfile = path.join(rootdir, "config", "config-"+process.env.TARGET+".js");

    //do this for each platform
    var configFilesToReplace = {
        "android" : "platforms/android/assets/www/js/config.js"
        ," ios" : "platforms/ios/www/js/config.js"
    };

    for(var platform in configFilesToReplace) {
        console.log("Modifying config for platform "+platform+", TARGET="+process.env.TARGET);
        var destfile = path.join(rootdir, configFilesToReplace[platform]);

        if (!fs.existsSync(srcfile)) {
            throw "Missing config file: "+srcfile;
        } else {
            console.log("copying "+srcfile+" to "+destfile);
            fs.createReadStream(srcfile).pipe(fs.createWriteStream(destfile));
        }
    }
} else {
    console.log("TARGET environment variable is not set.  Using default values.");
}

Android Notifications using Cordova and Ionic

So you want to add push notifications to your Cordova/Android/Ionic app? I’ll walk through it. Most of this entry applies to Cordova and Android, in case you’ve made the mistake of NOT using Ionic…

Basic Info

Android notifications work through Google GCM (Google Cloud Messaging). You can read the details at http://developer.android.com/google/gcm/index.html, but here’s the summary:

GCM Explained

Here’s the narrative version:  When your application starts up, the Android device registers with GCM server, which responds with a Registration ID.   The device then sends the registration ID to our Message Server, which stores it for future use.  Other information, such as the user’s name or location, can be sent for message targeting.

When we want to send out a notification, our Message Server sends the text to the GCM server, along with the registration ID and the API .  The GCM server relays info to the device.

The GCM Server

Setting up your Google GCM server should be a fairly quick process. It’s a simple mechanism from our perspective, but does a lot of the heavy lifting for us. Follow these directions to get it running: http://developer.android.com/google/gcm/gs.html. Be sure to make note of your GCM Project ID, and your API Key. The Project ID will go into your Android app configuration, for registration. The API Key is used to send notifications.

The Notification Server

There are any number of ways to do this, and from what I’ve seen, folks often use custom code for this. Of coruse there are some off the shelf products. But for development, I recommend the node-gcm project https://github.com/ToothlessGear/node-gcm. It’s a super-simple way to communicate with the GCM server. First install

npm install node-gcm

Next set up a node script similar to this one:

var gcm = require('node-gcm');
var message = new gcm.Message();

//API Server Key
var sender = new gcm.Sender('INSERT_YOUR_API_SENDER_KEY_HERE');
var registrationIds = [];

// Value the payload data to send...
message.addData('message', &quot;Hello Cordova!&quot;);
message.addData('title','Push Notification Sample' );
message.addData('msgcnt','2'); // Shows up in the notification in the status bar
message.addData('soundname','beep.wav'); //Sound to play upon notification receipt - put in the www folder in app
message.collapseKey = 'demo';
message.delayWhileIdle = true; //Default is false
message.timeToLive = 3000;// Duration in seconds to hold in GCM and retry before timing out. Default 4 weeks (2,419,200 seconds) if not specified.

// At least one reg id required
registrationIds.push('THIS_IS_THE_REGISTRATION_ID_THAT_WAS_GENERATED_BY_GCM');

/**
 * Parameters: message-literal, registrationIds-array, No. of retries, callback-function
 */
sender.send(message, registrationIds, 4, function (err, result) {
    console.log(result);
});

The Android App

As you know by now, you need a plugin to do just about anything in Cordova. So install the PushPlugin https://github.com/phonegap-build/PushPlugin. This should be the only plugin you need
for both Android and iOS. You might want to scan the instructions, but (as of now) they are both overly complicated and incomplete. For starters, you don’t need PlugMan to install. Just use:

cordova plugin add https://github.com/phonegap-build/PushPlugin.git

The coding was a little bit tricky, but I finally managed a functional script for Ionic notifications. See inline comments for more information on what’s going on here:

 //factory for processing push notifications.
angular.module('pushnotification', [])
   .factory('PushProcessingService', function() {
        function onDeviceReady() {
            console.info('NOTIFY  Device is ready.  Registering with GCM server');
            //register with google GCM server
            var pushNotification = window.plugins.pushNotification;
            pushNotification.register(gcmSuccessHandler, gcmErrorHandler, {&quot;senderID&quot;:gcmAppID,&quot;ecb&quot;:&quot;onNotificationGCM&quot;});
        }
        function gcmSuccessHandler(result) {
            console.info('NOTIFY  pushNotification.register succeeded.  Result = '+result)
        }
        function gcmErrorHandler(error) {
            console.error('NOTIFY  '+error);
        }
        return {
            initialize : function () {
                console.info('NOTIFY  initializing');
                document.addEventListener('deviceready', onDeviceReady, false);
            },
            registerID : function (id) {
                //Insert code here to store the user's ID on your notification server.
                //You'll probably have a web service (wrapped in an Angular service of course) set up for this.
                //For example:
                MyService.registerNotificationID(id).then(function(response){
                    if (response.data.Result) {
                        console.info('NOTIFY  Registration succeeded');
                    } else {
                        console.error('NOTIFY  Registration failed');
                    }
                });
            },
            //unregister can be called from a settings area.
            unregister : function () {
                console.info('unregister')
                var push = window.plugins.pushNotification;
                if (push) {
                    push.unregister(function () {
                        console.info('unregister success')
                    });
                }
            }
        }
    });

// ALL GCM notifications come through here.
function onNotificationGCM(e) {
    console.log('EVENT -&gt; RECEIVED:' + e.event + '');
    switch( e.event )
    {
        case 'registered':
            if ( e.regid.length &gt; 0 )
            {
                console.log('REGISTERED with GCM Server -&gt; REGID:' + e.regid + &quot;&quot;);

                //call back to web service in Angular.
                //This works for me because in my code I have a factory called
                //      PushProcessingService with method registerID
                var elem = angular.element(document.querySelector('[ng-app]'));
                var injector = elem.injector();
                var myService = injector.get('PushProcessingService');
                myService.registerID(e.regid);
            }
            break;

        case 'message':
            // if this flag is set, this notification happened while we were in the foreground.
            // you might want to play a sound to get the user's attention, throw up a dialog, etc.
            if (e.foreground)
            {
                //we're using the app when a message is received.
                console.log('--INLINE NOTIFICATION--' + '');

                // if the notification contains a soundname, play it.
                //var my_media = new Media(&quot;/android_asset/www/&quot;+e.soundname);
                //my_media.play();
                alert(e.payload.message);
            }
            else
            {
                // otherwise we were launched because the user touched a notification in the notification tray.
                if (e.coldstart)
                    console.log('--COLDSTART NOTIFICATION--' + '');
                else
                    console.log('--BACKGROUND NOTIFICATION--' + '');

                // direct user here:
                window.location = &quot;#/tab/featured&quot;;
            }

            console.log('MESSAGE -&gt; MSG: ' + e.payload.message + '');
            console.log('MESSAGE: '+ JSON.stringify(e.payload));
            break;

        case 'error':
            console.log('ERROR -&gt; MSG:' + e.msg + '');
            break;

        default:
            console.log('EVENT -&gt; Unknown, an event was received and we do not know what it is');
            break;
    }
}

Call it from here:

app.run(function(PushProcessingService) {
   //run once for the app
   PushProcessingService.initialize();
});

Hopefully this is helpful to someone!

Thanks to these folks (and others) who put together helpful blogs on this topic:

Cordova and AngularJS – Opening links in system browser

In the course of writing my Android/Cordova/Ionic app, I ran into another tricky requirement: Display HTML content from a web service, containing links. These links should open in the user’s preferred browser. In the end, there are several steps necessary to getting this right.

Step 1: Display unescaped HTML in the app.

When AngularJS binds content, special characters are “escaped”, so HTML tags are visible and of course non-functional. Fortunately AngularJS has a fix for this: ng-bind-html. Use this instead of ng-bind (which is equivalent to the double-bracket method). Your content will be slightly sanitized (which will be a problem later), but the links are preserved.

<div ng-bind-html="content.Description"></div>

At this point you’ll see some strange behavior: the links open inside webkit. So the user stays in your app, but there is no way to go back to your Angular HTML5 pages. In my case, even the back button did not work!

As with so many Cordova issues, “there’s a plugin for that.”

Step 2: Install the inappbrowser plugin

Despite its name, the inappbrowser plugin enables the behavior we’re looking for – opening a browser window EXTERNAL to our application. The following command will install it:

cordova plugin add org.apache.cordova.inappbrowser

In theory, all you have to do is set up your links as follows:

window.open('http://intown.biz', '_system', 'location=yes');
Take Note: There might be a better solution for step 3, that does not require Jquery. See comment by Rewon below.

 

Step 3: Apply the “inappbrowser” approach (part 1).

The next question is how to apply this to your downloaded content? Because of AngularJS, there are two issues:

  • JavaScript is “sanitized” (i.e. not rendered) by ng-bind-html. Otherwise, we would be able to write a filter to replace our ordinary Anchor tags with tags that invoke the javascript you see above.
  • I tried directives, but since I don’t have control over the HTML being passed in, that proved more complicated.  Depending on your circumstances, a directive may work for you.

I finally arrived at a two step process. First, use a simple custom filter to add ‘class=”ex-link”‘ to each section where we want the links to open in the external browser.

.filter('externalLinks', function() {
   return function(text) {
     return String(text).replace(/href=/gm, "class=\"ex-link\" href=");
   }
 })

The above filter would need to be more sophisticated if there are classes within the HTML that need to be preserved. Applying the filter in AngularJS, your HTML now looks like this:

<div ng-bind-html="content.Description | externalLinks"></div>

Step 3: Apply the “inappbrowser” approach (part 2).

Finally, we address the question of how to force our links to open in the _system window using JavaScript. For this we’ll resort to JQuery. I generally like to keep JQuery out of my projects, but in my case it was already included for a third-party tool. If anyone knows of a “pure Angular” approach to this, I’m all ears.

Using $timeout ensures that this handler is one of the last applied to the page.

$timeout(function () {
   $('.ex-link').click(function () {
     var url = $(this).attr('href');
     window.open(encodeURI(url), '_system', 'location=yes');
     return false;
   })
})

You might place the above code into the controller of the page that needs it, or in a factory containing utility functions. Don’t forget to inject $timeout in the appropriate place.

This two-step approach has advantages: obviously the “ex-link” class can be added directly to a template’s HTML, allowing you to place an “external link” anywhere in your app, and distinguish from your app’s internal links.

Of course there are some risks to displaying foreign HTML in a Cordova app. For my project, I have been assured that the HTML will be correct, but of course if it’s not, my app could break. So down the road I may add HTML integrity checking, but in the Agile tradition, I will only address this if it’s a real issue.