Android SDK Version: 2.5.4

Change log

Pre requisites

Finotes SDK supports android projects with minimum SDK version 14 (Ice-cream Sandwich) or above.

Integration

In-order to integrate Finotes in your Android project, add the code below to project level build.gradle


allprojects {
    repositories {
        jcenter()
        maven {
            url "s3://finotescore-android/release"
            credentials(AwsCredentials) {
                accessKey = <Access Token>
                secretKey = <Secret>
            }
        }
   }
}

You will be able to get Access Token, Secret from ‘Apps’ section in Finotes dashboard.

Then in app level build.gradle

implementation('com.finotes:finotescore:2.5.4@aar') {
    transitive = true;
}

Proguard

If you are using proguard in your release build, you need to add the following to your proguard-rules.pro file.

-keep class com.finotes.android.finotescore.* { *; }

-keepclassmembers class * {
    @com.finotes.android.finotescore.annotation.Observe *;
}

Add the below line incase you want to view the exact line number and source file name in your stacktrace.

-keepattributes SourceFile,LineNumberTable

Please make sure that the mapping file of your production build (each build) is backed up, inorder to deobfuscate the stacktrace from Finotes dashboard.
Location of mapping file: your-project-folder/app/build/outputs/proguard/release/mapping.txt

Initialize

You need to call the Fn.init() function in your application onCreate() function.

public class BlogApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

	Fn.init(this);
    }
}

DryRun

During development, you can set the dryRun mode, so that the issues raised will not be sent to the server. Every other feature except the issue sync to server will work as same.

public class BlogApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
	//Second parameter in Fn.init() toggles dryRun flag, which is false by default. 
	Fn.init(this, true , false);
    }
}
When preparing for production release, you need to unset the DryRun flag.

VerboseLog

There are two variations of logging available in Finotes, Verbose and Error. You can toggle them using corresponding APIs.
Activating verbose will print all logs in LogCat including error and warning logs.

public class BlogApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
	//Third parameter in Fn.init() toggles the verbose mode.
	Fn.init(this, false , true);
    }
}

Test

Now that the basic integration of Finotes SDK is complete,
Lets make sure that the dashboard and SDK are in sync.

Step 1.

Add Fn.reportIssue(this, “Test Issue”, Severity.MINOR) under Fn.init(this, false , true)

public class BlogApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        Fn.init(this, false , true);
        //Fn.reportIssue allows you to raise custom issues.
        //Refer Custom Issue section by the end of this documentation for more details.
        Fn.reportIssue(this, "Test Issue", Severity.MINOR);
    }
}
Step 2.

Now run the application in a simulator or real android device (with network connection).

Step 3.

Once the application opens up, open finotes dash.
The issue that we raised should be reported.

In-case the issue is not listed, make sure the right app and platform is selected at the top of the dashboard.

If you still cant find the issue in fintoes dashboard, Click Here.

You should remove the Fn.reportIssue() call, else every time the app is run, an issue will be reported.

public class BlogApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        Fn.init(this, false , true);
//        Fn.reportIssue(this, "Test Issue", Severity.MINOR);
    }
}

ReleaseBuild

Make sure that the dryRun and verbose flags are off in your release build along with the proguard rules specified above.

public class BlogApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

	Fn.init(this); //By default the dryRun and verbose flags are off.
    }
}

Please make sure that the mapping file of your production build (each build) is backed up, inorder to deobfuscate the stacktrace from Finotes dashboard.
Location of mapping file: your-project-folder/app/build/outputs/proguard/release/mapping.txt

Activity trail

When an issue is raised, inorder to get user screen flow for the current session, you need to extend your Activities and Fragments from ObservableActivity or ObservableFragment.

public class MainActivity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

Changes to,


public class MainActivity extends ObservableAppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

You may extend from the list below.

    ObservableAppCompatActivity 
    ObservableActivity 
    ObservableFragmentActivity 
    ObservableFragment

Once Activities and Fragments are extended from corresponding Observables, when an issue is raised, on Finotes dashboard, you will be able to view screen activity for 3 minutes before the issue was raised.

Activity Trail

    ActivityWelcome:onCreate     11:19:24:469	1131900928 FREE MEMORY (BYTES)
    MapActivity:onCreate         11:19:24:708	1131900928
    MapActivity:onStart          11:19:26:983	1131003904
    MapActivity:onResume         11:19:27:012	1130999808
    ActivityWelcome:onDestroy    11:19:28:515	1130622976
    MapActivity:onPause          11:20:17:806	1129603072

Custom Activity Trail

You can set custom activity markers in your android application using Fn.setActivityMarker(). These markers will be shown along with the activity trail when an issue is reported.

    Fn.setActivityMarker(PurchaseActivity.this, "clicked on payment_package_two");
Activity Trail

    ActivityWelcome:onCreate                            11:19:24:469	1131900928 FREE MEMORY (BYTES)
    MapActivity:onCreate                                11:19:24:708	1131458560
    MapActivity:onStart                                 11:19:26:983	1131003904
    MapActivity:onResume                                11:19:27:012	1130999808
    ActivityWelcome:onDestroy                           11:19:28:515	1130622976
    MapActivity:onPause                                 11:20:17:806	1129603072
    PurchaseActivity:onCreate                           11:20:18:106	1129230336
    PurchaseActivity:onStart                            11:20:18:404	1129222144
    PurchaseActivity:onResume                           11:20:18:906	1128136704
    PurchaseActivity:clicked on payment_package_two     11:20:24:235	1127759872

Listen for Issue

You can listen for and access every issue in realtime using the Fn.listenForIssue() API.
You need to add the listener in your Application class.

Fn.listenForIssue(new IssueFoundListener() {
    @Override
    public void issueFound(IssueView issue) {
	  .
	  .
	  .	
    }
});

You will be provided with an Issue object that contains all the issue properties that are being synced to the server, making the whole process transparent.
As this callback will be made right after an issue occurrence, you will be able to provide a positive message to the user.

Report Network Call Failure

You need to use the custom OkHttp3Client() provided by Finotes in your network calls.

    import com.finotes.android.finotescore.OkHttp3Client;
    ...
    ...
    
    client = new OkHttp3Client(new OkHttpClient.Builder()).build();

Issues like status code errors, timeout issues, exceptions and other failures will be reported for all network calls using custom OkHttp3Client() provided by Finotes, from your application .

Volley

In order to add OkHttp3Client to Volley, set the client to OkHttpStack().

OkHttpClient client = new OkHttp3Client(new OkHttpClient.Builder()).build();
RequestQueue mRequestQueue = 
	Volley.newRequestQueue(context,new OkHttpStack(client));
mRequestQueue.add(jsonObjReq);

You may use the OkHttpStack() from the following github gist

Incase you are using a singleton or AppController (Application class) to manage volley objects.

	AppController.getInstance().addToRequestQueue(jsonObjReq, tag_json_obj);

Go to that singleton or Application class and find the function where you have called

	public RequestQueue getRequestQueue() {
		if (mRequestQueue == null) {
		    mRequestQueue = Volley.newRequestQueue(getApplicationContext());
		}
        	return mRequestQueue;
   	}

Change this to,

	public RequestQueue getRequestQueue() {
		if (mRequestQueue == null) {
		    mRequestQueue = Volley.newRequestQueue(this,
			    new OkHttpStack(new OkHttp3Client(new OkHttpClient.Builder()
				    .connectTimeout(15, TimeUnit.SECONDS)
				    .retryOnConnectionFailure(false)).build()));
		}
        	return mRequestQueue;
    	}

You may use the OkHttpStack() from the following github gist
OkHttp3Client() is a custom class that Finotes provides, if the Finotes SDK is connected then it will be auto imported.

Retrofit

Adding OkHttp3Client to Retrofit is straight forward, Just call .client() in Retrofit.Builder()

OkHttpClient client = new OkHttp3Client(new OkHttpClient.Builder()).build();
Retrofit retrofit = new Retrofit.Builder()
	.baseUrl(API_URL)
	.client(client)
	.addConverterFactory(GsonConverterFactory.create())
	.build();

Incase you are already using a custom client,

	OkHttpClient.Builder currentBuilder = new OkHttpClient.Builder();
	... //Builder customization codes
	...
	OkHttpClient client = new OkHttp3Client(currentBuilder).build();	
retryOnConnectionFailure
connectTimeout
OkHttpClient.Builder builder = new OkHttpClient.Builder()
	.connectTimeout(15, TimeUnit.SECONDS)
	.retryOnConnectionFailure(false);
OkHttp3Client  client = new OkHttp3Client(builder).build();

Whitelist Hosts

Optionally, you may add @Observe annotation to the same Application class where Fn.init() function was called.
If Application class is annotated with @Observe, then, only issues from API calls to hosts listed in @Observe will be raised.

@Observe(domains = {
        @Domain(hostName = "host.com"),
        @Domain(hostName = "p.anotherhost.com", timeOut = 8000)
        })
public class BlogApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Fn.init(this, false , true);
    }
}

Optionally, You may provide a custom timeout for hostnames. This will raise an Issue for any network call to the ied hosts, that takes more than the specified time for completion.
Do note that specifying timeout will not interfere in any way with your network calls.

Mask Headers

You have an option to mask headers, incase they contain sensitive information.

@Observe(maskHeaders = {"X-Key", "X-Me"})
public class BlogApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Fn.init(this, false , true);
    }
}

or

@Observe(maskHeaders = {"X-Key"})
public class BlogApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Fn.init(this, false , true);
    }
}

This will mask any headers with matching name in both request and response headers before raising the issue to Finotes dashboard. The maskHeaders field is case insensitive.

Categorize Tickets (Optional)

You will be able to configure how API issues are categorized into tickets using a custom header “X-URLID”. You can set this header in your API calls and when they are reported to Finotes dashboard, issues from API calls with same “X-URLID” will be categorized into a single ticket.

       @Headers("X-URLID: loginapi")

or

       .addHeader("X-URLID","registrationapi")

Any issue in API calls with same X-URLID will be shown as a single ticket in Finotes dashboard.

Catch Global Exceptions.

In-order to catch uncaught exceptions, You may use Fn.catchUnCaughtExceptions().

@Override
protected void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Fn.init(this);
    Fn.catchUnCaughtExceptions();
}

Low Memory Reporting

Optionally, You may extend your Application class from ObservableApplication. This will report any app level memory issues that may arise in your application.

public class BlogApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
    }
}

Change to,

public class BlogApplication extends ObservableApplication {
    @Override
    public void onCreate() {
        super.onCreate();
    }
}

Custom Exceptions

You can report custom exceptions using the Fn.reportException() API.

try {
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("key","value");
} catch (JSONException exception) {
    Fn.reportException(this, exception);
}

Custom Issue

You can report custom issues using the Fn.reportIssue() API.

@Override
private void paymentCompleted(String userIdentifier, int type){
    //Handle post payment.
}

@Override
private void paymentFailed(String userIdentifier, String reason){
    Fn.reportIssue(this, "payment failed for "+reason);
    //Handle payment failure.
}

Function call

Finotes will report any return value issues, exceptions and execution delays that may arise in functions using Fn.call() and @Observe annotation.
A regular function call will be,

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    String userName = getUserNameFromDb("123-sd-12");

}

public String getUserNameFromDb(String userId){
    String userName = User.findById(userId).getName();
    return userName;
}

Function “getUserNameFromDb()” call needs to be changed to,

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    String userName = (String) Fn.call("getUserNameFromDb", this, "123-sd-12");
}

@Observe
public String getUserNameFromDb(String userId){
    String userName = User.findById(userId).getName();
    return userName;
}

You need to tag the function using @Observe annotation and the function should to be ‘public’.

This will allow Finotes to raise issue incase the function take more than normal time to execute, or if the function return a NULL value, or throws an exception.

Asynchronous Chained Function calls

Using chained function calls, you can detect when a functionality in your app fails.

All app features will have a start function and a success function, using Finotes SDK you will be able to chain both these functions.
You can chain functions using ‘nextFunctionId’ and ‘nextFunctionClass’ properties in @Observe annotation.

Use case: Add to cart

Lets take a look at an “add to cart” functionality in an typical ecommerce app.

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_product_listing);


	addToCartButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
		Fn.call("addItemToCart", ProductListingActivity.this, item.getId());
            }
        });

    }
    
    // Here function 'onItemAddedToCart' is expected to be called in under '5000' milliseconds.
    @Observe(nextFunctionId = "onItemAddedToCart",
            nextFunctionClass = ProductListingActivity.class,
	    expectedChainedExecutionTime = 5000)
    public boolean addItemToCart(String itemId){
	if(isValid(itemId)){
	     ... 
	     ...
	     return true;
	}
	return false;
    }
    
    @Override
    public void onApiCallComplete(JSONObject response){
	if(validResponse(response)){
	     Fn.call("onItemAddedToCart", ProductListingActivity.this, response.getString("id"));
	}
    }
    
    @Observe
    public void onItemAddedToCart(String itemId){
	showMessage(Messages.CARTED_SUCCESS);
    }
nextFunctionId/nextFunctionClass

Here, we have connected functions “addItemToCart” to “onItemAddedToCart” using ‘nextFunctionId’ and ‘nextFunctionClass’ in @Observe with expectedChainedExecutionTime set to 5000 milliseconds.

expectedChainedExecutionTime

Now as soon as “addItemToCart” is executed, Finotes will listen for “onItemAddedToCart” to be executed. If the same is not called within 5000 milliseconds after the execution of “addItemToCart”, an issue will be raised that says the function “onItemAddedToCart” is never called.
Now if the function “onItemAddedToCart” is executed after 5000 milliseconds, another issue will be raised that says the function “onItemAddedToCart” is called with a delay.

Note,

It is recomended to have no user activity when chaining 2 funtions. lets call functions, A and B. i.e After execution of function A, no user activity (like a click or input) should be required for execution of B.

Use case: Chat

Lets take a look at a “chat” functionality in an typical chat app.

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat);


	sendButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
		Fn.call("sendChat", ChatActivity.this, chatBox.getText().toString());
            }
        });

    }
    
    // Here function 'onChatSent' is expected to be called in under '2000' milliseconds (default value).
    @Observe(nextFunctionId = "onChatSent",
            nextFunctionClass = ChatActivity.class)
    public boolean sendChat(String message){
	if(isValid(message)){
	     syncMessage(message);
	     return true;
	}
	return false;
    }
    
    @Override
    public void onApiCallComplete(JSONObject response){
	if(validResponse(response)){
	     Fn.call("onChatSent", ChatActivity.this, response.getString("id"));
	}
    }

    @Observe
    public void onChatSent(String chatMessageId){
	chatSyncConfirmed(chatMessageId);
    }

Here ‘onChatSent(String chatMessageId)’ should be called within 2000 milliseconds(default value) after execution of ‘sendChat(String message)’.
If the function ‘onChatSent’ is not called or is delayed then corresponding issues will be raised and reported.

boolean returns false

Here in ‘sendChat’ function, if it returns false, an issue will be raised with the function parameters, which will help you dig more into what could have gone wrong.

Test

In-order to check if you have implemented function chaining correctly, set break points inside the chained functions then run application in your simulator or device, and use your application, if you have implemented correctly, You will hit these breakpoints correctly.

You can control multiple parameters in @Observe annotation.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    String userName = (String) Fn.call("getUserNameFromDb", this, "123-sd-12");
}

@Observe(expectedExecutionTime = 1400)
public String getUserNameFromDb(String userId){
    String userName = User.findById(userId).getName();
    return userName;
}
expectedExecutionTime

Here the expectedExecutionTime for the function “getUserNameFromDb” has been overriden to 1400 milliseconds (default was 1000 milliseconds). If the database query takes more than 1400 milliseconds to return the value or if the returned “userName” is NULL, then corresponding issues will be raised.

Exception in function

Issue will be raised if an exception is thrown inside the function that is annotated with @Observe and is called using Fn.call().

Returns NULL

If you expect a function to return NULL value then, to exempt that function from raising “returns NULL” issue, use “expectNull” field in @Observe annotation.

    @Observe(expectNull = true)
    public String getUserNameFromDb(String userId){
    }
Expected Boolean value

If the function annotated with @Observe returns a Boolean value, you may set a expceted value for the same and during execution if the function returns a value other than the expected value, an issue will be raised.

    @Observe(expectedBooleanValue = false)
    public Boolean isValidUser(JSONObject response){
    }
Return value range

If the function annotated with @Observe returns a number or decimal value, you may set a range for the same and during execution if the function returns a value outside the specified range, an issue will be raised.

    @Observe(min = 10, max = 15)
    public static long getUserCountFromJSON(JSONObject response){
    }
    
    or
    
    @Observe(min = 1.1, max = 1.9)
    public static float getUserCountFromJSON(JSONObject response){
    }
Mask parameters

Inorder to mask function parameters before raising an issue, you may use ‘mask’ field in @Observe.

    @Observe(mask = {0})
    public static long getUserCountFromJSON(JSONObject response){
    }

Here, incase an issue is raised in function ‘getUserCountFromJSON(JSONObject)’, the JSONObject value will be masked.
You need to provide parameter position starting from 0 in the ‘mask’ field in @Observe.

@Observe(mask = {1})
public String getUserNameFromDb(String email, String token){
    String userName = User.findUniqueByProperties(email,token).getName();
    return userName;
}

Here the field ‘String token’ will be masked during an issue raise.

Function overloading

You need to provide an explicit ID in @Observe annotation to the function incase of function overloading.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Here the function name "getUserNameFromDb" is passed in Fn.call().
    // This will trigger the getUserNameFromDb(String userId) function.    
    String userName = (String) Fn.call("getUserNameFromDb", this, "123-sd-12");

    // Here the id "getUserNameFromDBWithEmailAndToken" specified in 
    // @Observe annotation is passed in Fn.call().
    // This will trigger the getUserNameFromDb(String email, String token) function.    
    String userName = (String) Fn.call("getUserNameFromDBWithEmailAndToken", 
						this, "support@finotes.com", "RENKDS123S");
}

@Observe(expectedExecutionTime = 1400)
public String getUserNameFromDb(String userId){
    String userName = User.findById(userId).getName();
    return userName;
}

@Observe(id = "getUserNameFromDBWithEmailAndToken", expectedExecutionTime = 1400)
public String getUserNameFromDb(String email, String token){
    String userName = User.findUniqueByProperties(email,token).getName();
    return userName;
}
ID

If a custom id is specified in @Observe annotation, then while calling that function, you need mandatorily to pass the id and not the function name.
If no custom id is specified in @Observe annoatation, then you can pass the function name itself as the id.

@Override
protected void onCreate(Bundle savedInstanceState) {

//  Function name itself is provided as function getUserNameFromDb(String userId) 
//	doesnt have any explicit id in its @Observe annotation
    Fn.call("getUserNameFromDb", this, "123-sd-12");

//  ID 'getUserNameFromDBWithEmailAndToken' is provided as function 
//      getUserNameFromDb(String email, String token) 
//	does have an explicit id in its @Observe annotation
    Fn.call("getUserNameFromDBWithEmailAndToken", 
						this, "support@finotes.com", "RENKDS123S");
}

@Observe
public String getUserNameFromDb(String userId){
}

@Observe(id = "getUserNameFromDBWithEmailAndToken")
public String getUserNameFromDb(String email, String token){
}

Function in Separate Class file

If the function is defined in a separate class file and needs to be invoked, then instead of passing “this” you can pass the corresponding object in Fn.call()

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    DBUtils dbUtils = new DbUtils();
    String userName = (String) Fn.call("getUserNameFromDb", dbUtils, "123-sd-12");

}

public class DBUtils {

    @Observe(expectedExecutionTime = 1400)
    public String getUserNameFromDb(String userId){
        String userName = User.findById(userId).getName();
        return userName;
    }
}

Static Function calls

For static functions, pass corresponding .class instead of object in Fn.call() to invoke static method.

public void onHttpCallCompleted(JSONObject httpResponseJSONObject) {
    
    long userTimestamp = (long) Fn.call("getUserTimestampFromJSON", DBUtils.class,
    							httpResponseJSONObject);

}

public class DBUtils {

    @Observe(expectedExecutionTime = 2000)
    public static long getUserTimestampFromJSON(JSONObject response){
        long userTimestamp = processJSONAndFindUserTimestamp(response);
        return userTimestamp;
    }

    @Observe(expectedExecutionTime = 1400)
    public String getUserNameFromDb(String userId){
        String userName = User.findById(userId).getName();
        return userName;
    }
}