Showing posts with label android app. Show all posts
Showing posts with label android app. Show all posts

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.


Monday, 10 February 2014

What was Google Thinking When They Did This?

Yeah, I'm talking about the totally ridiculous 'Rate this app to get recommendations' gig by Google.

So here is what happened to me the other day. I open up Play Store, and I get the Rate this app... thingy and its asking me to rate an app I uninstalled about 6 months ago. Now I download about an app a day, and I have no freekin idea about that app, was it good? Did it crash on me? Did I get bored of it? Umm.. no.. no idea. But I still want the recommendations.. So being the good guy I am I give it 5 stars and proceed. BUT.. the problem is we are not living in a world filled with good guys, I am sure many would have just given it a one star rating and moved on. Yeah, check out this link by androidpolice.

This is where my problem lies as a developer. I have a stickiness ratio of about 40% on my apps, which means 60% of my installs result in uninstalls. And imagine google asking these guys to rate my app.. I mean c'mon some of them may not even have used my app. 

I see what Google's trying to do here, they want apps rated which helps them in ranking the apps, fair enough. NO.. not fair. Cause a majority of these ratings are not even legit. They've been given under the temptation of getting more recommendations like my app. 

All I can do is hope that there are more good guys like me out there.

Saturday, 8 February 2014

Reducing Image Sizes to Reduce the Size of Your apk

Wanted to write a quick blog post on this wonderful tool I found called ImageOptim. So the apk size of my app, BreakFree, had gone to 7mb. Which I was not very comfortable with. Many users, being on limited data, are fussy about downloading apps over 2-3 mb. My app was clearly well over. 

One of the things I had never considered before was reducing the size of my images. And the most easiest ways I found was by using this tool called ImageOptim. You can get it at imageoptim.com. Its super easy to use, unlike the other tools I explored. And it does a decent job with the images as well.

All my images are .pngs saved mostly on full compression out of GIMP. By processing them through ImageOptim I got an average reduction of about 16%-17% on my file sizes. I managed to reduce my apk size to 5.5mb. A far cry from the ideal 3 mb, but at least its better than 7mb.

Monday, 20 January 2014

BreakFree is Officially Published

Hello peoples. Its been two days since the launch of BreakFree, and the response has been decent. I've collected about 75 odd downloads with seven people rating it (all 5 stars as yet, woohoo). I would have obviously liked the download numbers to be better, don't we all hope for our apps going viral, but this is still ok. Considering this is a beta launch and I did not marketing whatsoever, except for posting comments in some articles and Youtube videos.

I have already made two updates due to the bugs found by users. I have also created quick and dirty website (it seriously was quick, about 3 hrs to learn Wordpress and create the website). You can check it out by clicking the 'BreakFree' link under 'My Apps'.

My strategy is to wait till I get about 500 downloads, after which I start my heavy duty promotions. I am still coming up with a plan for the promotion piece. Will share that you as well.

Attaching a few screenshots of the app. You can get more detail by visting the website or the Google Play Store page.

Have fun.