Friday, 9 September 2016

The Importance of a Rating Pop Up in your Mobile App

We all know how important it is for our apps to get rated. Ratings help users choose your app over the competition. Ratings also play a big part in the your app rankings (cannot overstate the importance of this point, even a small dip in your ratings can get an app in a crowded space plummeting down). So how important is it to have a pop up that asks a user to rate the app? Based on my experiment, very important. 

So here is what I did. I've always had a pop up, in my app BreakFree, that asks the user to rate my app and takes them to the Google Play Store page of the app. I removed this pop up for a month (29th Jul to 28th Aug) and then re-instated the pop up. Look at the graph below to see to what extent this affected the number of ratings I received per day.




As you can see there is a huge increase in the number of ratings 29th Aug onwards. To put this in numbers, I got 0.97 ratings/day for the time period when I did not have a rating pop up. And from the day I added this back, my ratings/day jumped up to 3.13. Not only this, but the rating itself improved from 3.5 to 4.1. This is because when you don't ask for a rating, only the unsatisfied user will go out of his way to leave a bad comment with a 1 star rating.

So this is a no brainer, your app must always ask the user for a rating. There are numerous ways on when and how to do this. Will reserve that for another post.

Sunday, 7 September 2014

The Relationship Between Downloads and In-app Purchases

As mentioned in my previous blog post, I've been doing a lot of analysis on my apps, especially the popular ones (BreakFree and Fuel Buddy). One of the points I wanted to dive into was the nature of in-app purchases. How are these related to downloads? At what point do people go for in-app purchases? Does an increase in downloads always translate to higher purchases? 

The common theory is that increase your downloads and your purchases will increase. This is true but with a caveat. Let me explain. 

Look at the graph below. The red bars represent downloads and the blue bars are in-app units sold (the actual number was obviously lot lower, it has been multiplied by x to get both the bars in proportion). Now as you can see the downloads increased from Jan to April, and so did the in-app purchases, no surprise here. But look at June, though the downloads fell drastically the purchases actually increased! The fall in the purchases only triggered in July. This can again be seen in August, though the downloads increased, the in-app purchases fell.



From the above findings we can deduce that in-app purchases do follow the download trend, but there is a lag of a month. The plausible explanation could be that the users only purchase after trying out the app for a fair amount of time (on an average one month).

So the next time you see a jump in downloads but not in in-app purchases, don't worry, it will soon follow :-)

Please keep in mind that this study was done on Fuel Buddy, which is a stable, high utility product. The above study may not hold true for games and novelty products.

Friday, 22 August 2014

A Case Study of What Went Wrong With my App

One of my apps, Fuel Buddy, has not been doing well lately. And by doing well, I mean the downloads have been low, and rankings are on a steady decline. To take corrective action I took some desperate measures like adding a new feature, changing screenshots, etc. But there was no improvement. So I decided to analyze thoroughly what the problem could be. I gave myself an entire day to figure out what could be going wrong. What I found was surprising to say the least. The reasons were a combination of multiple points and not just one. I took note of my findings to make sure I don't repeat the mistakes again. Sharing them here.

Fuel Buddy Downloads Case Study

  • This one is obvious, but needs to stated as the first point. Rankings and downloads go hand in hand. One affects the other. This one clearly has a spiral effect. The sooner you nip the bud the better.
  • A slight drop in ratings over two days plummeted my rankings, which led to a drop in downloads (see point 1). Case in point, my app got an average rating of 2 and 3 on the 6th and 7th of May. This led to a drop in downloads by almost 30%!! Though I have not seen the opposite being true, a spike in ratings leading to a better ranking. None the less, avoid low ratings at all costs.
  • Change in the app title can lead to big drop in installs and high uninstalls. This can only be experimented with over a long period, a few months maybe. Eg, I Changed Title to FuelBuddy-Car Service, Mileage, and saw the rate of uninstalls rise. Plausible reason can be that people expected the app to be more of a car service app, whereas it is more on the lines of a mileage logger. Changed the title back to Fuel Buddy - Car Mileage Log and the rate of uninstalls reduced.
  • If you see your uninstalls go up, the first thing to do is check your crashes, we tend to ignore this since they don't show up in the Developer Console, but it is a good idea to check them with a analytics software like Google Analytics or Flurry. Make sure to implement these in your app, because most people don't bother clicking the 'Send Report' button, they just uninstall the app.
The one thing I did to desperately get back on track was churn out random solutions in the hope that I fix my app. This was mainly due to the amount of realtime information available to us in the form of downloads, ratings, reviews, in-app purchases, etc. Don't get bogged down by daily data. The truth is that daily fluctuations can be caused by external factors. It all makes sense when you zoom out, and look at the big picture. Study the trend over a week or month. That will give you the real root cause which you can act upon.

Hope these points help you out. I'll be doing another case study of my Facebook Ad campaign on one of my apps. Stay tuned for that.

Tuesday, 10 June 2014

Google Cloud Messaging for Dummies!

So I am working on something pretty big for BreakFree. I am introducing a lot of gamification in the app. To give you an idea, my users will be able to view their Facebook friends on BreakFree, what their addiction score is and they can also challenge their friends to see who can get a better score. In any case, the long and short of it is that I needed GCM in my app. And to be very honest I couldn't find a decent tutorial on it. Most of them dated back to the C2DM era. So I thought of putting one together. 

This is going to be the simplest GCM implementation ever, by itself it's not going to add much value to your app, but you should have a skeleton ready after which you can add the real meat. I assume you know a little bit of Android programming and you won't cry when you see PHP scripting. I am assuming you know what GCM is intended to do. If not you can read it here.

1. Register app on Google Developers Console
Ok so before we start with the fun stuff you will need to register your app on the Google Developers Console by creating a project, enabling the GCM service and obtaining an API key. Follow the Getting Started section of GCM (GCM - Getting Started). Please make note of the Project Number and API Key got from this step.

2. Register user's device with the GCM service
Before we dig into this step you must understand that for GCM to work every device associated with your app needs to have a unique ID provided by the GCM service. This ID will be used by you to send notifications to these devices. To register you need to get an instance of GoogleCloudMessaging and run the register method. But you need to run this on a separate thread, hence we use a AsyncTask. Check the code below:

//async class to register with GCM in background
private class registerGCM extends AsyncTask<Void, Void, String> {
GoogleCloudMessaging gcm;

@Override
protected String doInBackground(Void... params) {
gcm = GoogleCloudMessaging.getInstance(ctx);
try {
  String regId = gcm.register(ctx.getString(R.string.google_project_id));
  
  //store reg id in SP
  spEdit.putString(ctx.getString(R.string.SPCRegID), regId);
  spEdit.commit();
catch (IOException e) {
  e.printStackTrace();
}
return null;
}

protected void onPostExecute(String regid) {
  //call function to update fb_id and then send data to server
  new storeRegID().execute("update fb_id");
}
}

In the code above I create an instance of GoogleCloundMessaging (ctx is my Context variable), register the device on GCM by providing the project number got in Step 1. I store the ID in SharedPreferance. And finally in onPostExecute() I call the another AsyncTask to store the ID on my server (described below)

3. Send GCM Reg ID on our server
We need to store our users reg ID's, provided by GCM, on our server. These ID's then need to be looked up whenever a user needs to send a notification to another user, or when you want to send notifications to your users from the server. Here is the Android code that sends the reg ID along with the user's name (this can be any unique identifier for your user).

//async class to send data reg id and unique id to server
private class sendDataToServer extends AsyncTask<String, Void, String>{

@Override
protected String doInBackground(String... vals) {

    //get reg id
    String regId = spObj.getString(ctx.getString(R.string.SPCRegID), null);

   //get name
   String name = "Mrigaen Kapadia"

   // add values to be sent
   List<NameValuePair> params = new ArrayList<NameValuePair>();
   params.add(new BasicNameValuePair("name", name));
   params.add(new BasicNameValuePair("regId", regId));

   // getting JSON Object
   JSONParser jsonParser = new JSONParser();
   JSONObject json = jsonParser.makeHttpRequest(ctx.getString(R.string.php_send_data), "POST",        params);

   // check for success tag
   try {
            int success = json.getInt("success");
            String message = json.getString("message");                
               
            if (success == 1) {                
                Toast.makeText(ctx, "Data Saved on Server", Toast.LENGTH_LONG).show();
            }
            // failed to create product
            else {
                Toast.makeText(ctx, "Failed to Save Data on Server", Toast.LENGTH_LONG).show();
            }
   } catch (JSONException e) {
                e.printStackTrace();
   }

  return null;
  }
}

Ok, so simple stuff, we get the reg ID stored in Step 2, and any other unique id of the user, like his name, Facebook id, etc. Mind you it is this unique id that other users will use to send a notification to this user. Both these are passed into a function of the JSONParser class (shown below), along with the name of the PHP script which we need to call and "POST" constant.

In the try/catch block we check if the posting on the server was a success by capturing the value retrieved from the PHP script. You will understand this better when you see the JSONParser class and the PHP script.

4. JSONParser Class
This is a utility class created to parse the information (read the NameValuePairs) sent to it and send over this information to the PHP script mentioned as a parameter. Lets see how this is done.


public class JSONParser {
    static InputStream is = null;
    static JSONObject jObj = null;
    static String json = "";

    // constructor
    public JSONParser() {}

    // function get json from url   
    public JSONObject makeHttpRequest(String url, String method, List<NameValuePair> params) {
     
        // Making HTTP request
        try {

            // check for request method
            if(method == "POST"){
                // defaultHttpClient
                DefaultHttpClient httpClient = new DefaultHttpClient();

                // send over params to url
                HttpPost httpPost = new HttpPost(url);
                httpPost.setEntity(new UrlEncodedFormEntity(params));
                
                //get response from server and store in a Input Stream
                HttpResponse httpResponse = httpClient.execute(httpPost);
                HttpEntity httpEntity = httpResponse.getEntity();                
                is = httpEntity.getContent();
            }          

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
             //read the response from the server
            BufferedReader reader = new BufferedReader(new InputStreamReader(is, "iso-8859-1"), 8);
            StringBuilder sb = new StringBuilder();
            String line = null;
            while ((line = reader.readLine()) != null) { 
                sb.append(line + "\n");
            }
            is.close();
            json = sb.toString();
        } catch (Exception e) {
            Log.e("Buffer Error", "Error converting result " + e.toString());
        }

        // try parse the string to a JSON object
        try {        
            jObj = new JSONObject(json);
        } catch (JSONException e) {
            Log.e("JSON Parser", "Error parsing data " + e.toString());
        }

        // return response from server back to calling function
        // this should ideally contain values for "success" and "message" from the server
        return jObj;

    }
}

As you can see, this all important class gets the NameValuePairs, sends them as a HttpPost to the URL specified. And waits for a response, which is then received by the calling function.

5. PHP Script to Store Name and RegID
Ok, so here we require some php scripting. Don't worry this is not too difficult. I didn't know any php before I started working on this update for BreakFree. 

<?php
ini_set('display_errors', 'On');
error_reporting(E_ALL);
/* The following code will create a user in the breakfree_users table */

// array for JSON response

$response = array();

// check for required fields

if (isset($_POST['name']) && isset($_POST['regId'])) {

    $name = $_POST['name'];
    $regId = $_POST['regId'];
    $notification = False;
    $num_rows = 0;
    
    // include db config class
    require_once __DIR__ . '/db_config.php';

    // connecting to db
    $mysqli = new mysqli(DB_SERVER, DB_USER, DB_PASSWORD, DB_DATABASE);
           
    // inserting a new row
    if($stmt = $mysqli->prepare("INSERT INTO table(name, reg_id) VALUES(?, ?)")){
   
       // Bind the variables to the parameter as  strings. 
       $stmt->bind_param("ss", $name, $regId);
   
      // Execute the statement.
      $result = $stmt->execute();
       
      // Close the prepared statement.
      $stmt->close();
   }        
    
    //if record inserted/updated correctly
    if ($result) {
        // successfully inserted into database
        $response["success"] = 1;
        $response["message"] = "User successfully created.";

        // echoing JSON response
        echo json_encode($response);
    }
    //record failed to be updated 
    else {
        // failed to insert row
        $response["success"] = 0;
        $response["message"] = "Oops! An error occurred.";

        // echoing JSON response
        echo json_encode($response);
    }

else {
    // required field is missing
    $response["success"] = 0;
    $response["message"] = "Required field(s) is missing";

    // echoing JSON response
    echo json_encode($response);
}

?>

As you can see this is a bare-bones script. You will obviously need to add some checks like if the record exists, etc. But this should suffice for our tutorial.

6. Send Unique ID of Receiver to the Server
To understand this piece, imagine that you are now on the sender's instrument. And this piece of code is run on the person's device who wants to send a notification to his friend. In this step we identify the name of the friend who needs to receive this notification along with the message and send it to our server. This name is on our server from Step 3.


//Async task to send unique id of GCM receiver to our sender

private class sendChallenge extends AsyncTask<String, Void, String> {

    String receiver = "Mrigaen Kapadia";
    String sender = "John Wayne";
    String message = "I need your help!";

    List<NameValuePair> params = new ArrayList<NameValuePair>();
    params.add(new BasicNameValuePair("receiver"receiver));
    params.add(new BasicNameValuePair("sender", sender);
    params.add(new BasicNameValuePair("message"message)); 

   // getting JSON Object
   // Note that create product url accepts POST method
   JSONParser jsonParser = new JSONParser();
   JSONObject json = jsonParser.makeHttpRequest(ctx.getString(R.string.php_send_name), "POST",   params);

    // check for success tag
    try {
           int success = json.getInt("success");                
               
           if(success == 1)
               return "success";
           else
               return "fail";                                                
      } 
      catch (JSONException e) {
                e.printStackTrace();
      }

      return "fail";
}

protected void onPostExecute(String result) {
     if (result.equals("success"))
            Toast.makeText(ctx, "name of receiver sent", Toast.LENGTH_SHORT).show();                        
      // failed to send name
      else
            Toast.makeText(ctx, "failed to send name to server", Toast.LENGTH_SHORT).show();
    }
}

Ok, so the name, as explained before is any unique ID that is stored by your program for individual users. The sender passes on the name of the receiver to our server, using the JSONParser class.

7. Send Reg ID and Other Params to GCM
This is probably the most important step in the tutorial. Here our PHP script picks up the receiver's name sent in Step 6, gets the corresponding Reg ID (stored in Step 5), and sends the Reg ID the message and the sender's name to GCM

<?php
ini_set('display_errors', 'On');
error_reporting(E_ALL);

/* The following code will receive the receivers name and other data to be sent and send the necessary information to GCM */

//array for JSON response
$response = array();

// include db connect class
require_once __DIR__ . '/db_connect.php';

//API Key
$api_key = <Your API Key from Step 1>;

// check for required fields
if (isset($_POST['receiver']) && isset($_POST['sender']) && isset($_POST['message'])) {
$receiver = $_POST['receiver'];
$sender = $_POST['sender'];
$message = $_POST['message'];
//get registration id for receiver
$result_reg_id = mysql_query("SELECT reg_id FROM breakfree_users WHERE name = '$receiver'");
    $num_rows = mysql_num_rows($result_reg_id);
   
    //reg id exists for b_fb_id
    if($num_rows > 0){
    $row = mysql_fetch_array($result_reg_id, MYSQL_ASSOC);
    $reg_id = $row["reg_id"];    
   
    // Set other POST variables for GCM
$url = 'https://android.googleapis.com/gcm/send';
$fields = array("registration_ids" => array($reg_id), "data" => array("sender" => $sender, "message" => $message);
$headers = array("Authorization: key=$api_key", "Content-Type: application/json");
// Open connection
       $ch = curl_init();
 
       // Set the url, number of POST vars, POST data
       curl_setopt($ch, CURLOPT_URL, $url);  
       curl_setopt($ch, CURLOPT_POST, true);
       curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
       curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
       
       // Disabling SSL Certificate support temporarly
       curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
 
       curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
 
       // Execute post
       $result = curl_exec($ch);
       if ($result === FALSE) {
           die('Curl failed: ' . curl_error($ch));
       }
 
       // Close connection
       curl_close($ch);        
       
       // successfully inserted into database
       $response["success"] = 1;
       $response["message"] = "Challenge sent";
               
       // echoing JSON response
echo json_encode($response);              
    }
    //no reg id found for b_fb_id
    else{
    // required field is missing
    $response["success"] = 0;
    $response["message"] = "Reg Id not found for Challenger";

    // echoing JSON response
echo json_encode($response);    
    }
}
else {
    // required field is missing
    $response["success"] = 0;
    $response["message"] = "Required field(s) is missing";

    // echoing JSON response
    echo json_encode($response);
}       
?>

The PHP script above picks up the receiver's name, sender's name and message sent by the sender in Step 6. It then looks up the Reg ID of the receiver (this should have been stored in Step 5). After this it sends off this information to GCM. The GCM url as shown above is 'https://android.googleapis.com/gcm/send'. The registration id needs to passed onto GCM in an array with the 'registration_ids'. Any other information, in our case sender and message, needs to passed in the same array with the key 'data'. Also pay close attention to the header format. This needs to be 'Authorization: key=$api_key", "Content-Type: application/json'. There are other options you can use. Visit the official Android page to know more.

8. Finally, Receive the Message on the Receiver's Handset
Now imagine you are on your receiver's handset. We need this piece of code to receive the message from GCM sent by the sender. First we must declare a Broadcast Receiver in our manifest to intercept the GCM message:

Add the following permissions:
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission
        android:name="<your package name>.C2D_MESSAGE"
        android:protectionLevel="signature" />

Add the receiver:
<application...
        <receiver
            android:name=".GCMBroadcastReceiver"
            android:exported="true"
            android:permission="com.google.android.c2dm.permission.SEND" >
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" /> 
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                <category android:name=<your package name> />
            </intent-filter>
        </receiver>
</application>

The GCMBroadcastReceiver class will look something like this:
public class GCMBroadcastReceiver extends BroadcastReceiver {

     @Override
      public void onReceive(Context ctx, Intent i) {
              Bundle extras = i.getExtras();
              
              //get sender name and message
              String sender = extras.getString("sender");
              String message = extras.getString("message");

              String notification = sender+" says "+message;

              //create notification or whatever here....
        }
}

As seen above the GCM parameters are received in the Intent passed onto the receiver. You can extract them with the keys you passed in the 'data' constant in Step 7. From here you can do whatever you need to with the received data, ideally you would create a notification or something like that.

Hope this simplified GCM tutorial helped you. Do leave your feedback/questions in the comments.

Until next time.


Wednesday, 19 March 2014

The All Important setDeleteIntent for Notifications

My app, BreakFree, notifies the user in case of any event like using the phone for a total of 1 hour, or launching a particular app 15 times or more, etc. Now, there is also an option in my app wherein the user gets to see his phone addiction score as a persistent notification. This is updated at every unlock. My problem here was that I did not want to overwrite the event notification with the addiction score notification till the user clears the event notification himself. 

Now, call me dumb, but for the love of God I found it impossible to find documentation on how to detect the user clearing the notification. After a lot of searching I found the missing link. Actually there are two. setDeleteIntent and setContentIntent.

Here is how I coded the solution:

1) In my function which updates my notifications, I created two pendingIntents, one two be called when the user deletes my notification (swiping it, or using the Clear All button). And the second to be called when the user clicks the notification. Both call the same receiver class (NotificationReceiver), but with different Extras. Pay particular attention to the flags for the Pending Intent.

//create pending intent to be called when user deletes the notification
Intent notiDeleted = new Intent(ctx, NotificationReceiver.class);
notiDeleted.putExtra(ctx.getString(R.string.noti_deleted), true);
PendingIntent notiDeletedIntent = PendingIntent.getBroadcast(ctx, 0, notiDeleted, 0);
   
//create pending intent to be called when user clicks the notification
Intent notiClicked = new Intent(ctx, NotificationReceiver.class);
notiClicked.putExtra(ctx.getString(R.string.noti_clicked), true);
PendingIntent notiClickedIntent = PendingIntent.getBroadcast(ctx, 1, notiClicked, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT);


2) I then created a SharedPreference flag saying that the notification is still present in the notification tray. This flag tells my code not to show the second notification.

//create a flag that an event noti is being shown
spEdit.putBoolean(ctx.getString(R.string.SPCEventNotiVisible), true);
spEdit.commit();

3) Build the notification. Use the setDeleteIntent and setContentIntent.
// build notification
NotificationCompat.Builder nb = new NotificationCompat.Builder(ctx);
nb.setSmallIcon(R.drawable.ic_jump_outline);
nb.setContentIntent(notiClickedIntent);
nb.setDeleteIntent(notiDeletedIntent);
nb.setAutoCancel(true);

Notification bfN = nb.build();

4) Next we create our receiver class.
public class NotificationDeleteReceiver extends BroadcastReceiver{

@Override
public void onReceive(Context context, Intent intent) {

  //make flag to noti not shown
  SharedPreferences spObj =
  context.getSharedPreferences(context.getString(R.string.SPPrevSetting), 0);
  SharedPreferences.Editor spEdit = spObj.edit();
  spEdit.putBoolean(context.getString(R.string.SPCEventNotiVisible), false);
  spEdit.commit();

  //if called from user click then show app
  if(intent.getExtras().containsKey(context.getString(R.string.noti_clicked))){
Intent i = new Intent(context, MainScreen.class);
        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |            Intent.FLAG_ACTIVITY_CLEAR_TOP | 
        Intent.FLAG_ACTIVITY_SINGLE_TOP);                
i.putExtra(context.getString(R.string.noti_on_click),                              intent.getExtras().getString(context.getString(R.string.noti_on_click)));
context.startActivity(i);
}
  }

}

As you can the receiver changes the SharedPreference flag to false regardless of whether the notification was cleared or clicked. It then checks the extras and if the notification was clicked it then opens up the app.

Hope this helps you guys.