Android 4?



Chapter 138: Gesture Detection
Section 138.1: Swipe Detection
public class OnSwipeListener implements View.OnTouchListener {
private final GestureDetector gestureDetector;
public OnSwipeListener(Context context) {
gestureDetector = new GestureDetector(context, new GestureListener());
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
private final class GestureListener extends GestureDetector.SimpleOnGestureListener {
private static final int SWIPE_VELOCITY_THRESHOLD = 100;
private static final int SWIPE_THRESHOLD = 100;
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
float diffY = e2.getY() - e1.getY();
float diffX = e2.getX() - e1.getX();
if (Math.abs(diffX) > Math.abs(diffY)) {
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) >
SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
onSwipeRight();
} else {
onSwipeLeft();
}
}
} else if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) >
SWIPE_VELOCITY_THRESHOLD) {
if (diffY > 0) {
onSwipeBottom();
} else {
onSwipeTop();
}
}
return true;
}
}
public void onSwipeRight() {
}
public void onSwipeLeft() {
}
public void onSwipeTop() {
}
GoalKicker.com – Android™ Notes for Professionals 777
public void onSwipeBottom() {
}
}
Applied to a view...
view.setOnTouchListener(new OnSwipeListener(context) {
public void onSwipeTop() {
Log.d("OnSwipeListener", "onSwipeTop");
}
public void onSwipeRight() {
Log.d("OnSwipeListener", "onSwipeRight");
}
public void onSwipeLeft() {
Log.d("OnSwipeListener", "onSwipeLeft");
}
public void onSwipeBottom() {
Log.d("OnSwipeListener", "onSwipeBottom");
}
});
Section 138.2: Basic Gesture Detection
public class GestureActivity extends Activity implements
GestureDetector.OnDoubleTapListener,
GestureDetector.OnGestureListener {
private GestureDetector mGestureDetector;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mGestureDetector = new GestureDetector(this, this);
mGestureDetector.setOnDoubleTapListener(this);
}
@Override
public boolean onTouchEvent(MotionEvent event){
mGestureDetector.onTouchEvent(event);
return super.onTouchEvent(event);
}
@Override
public boolean onDown(MotionEvent event) {
Log.d("GestureDetector","onDown");
return true;
}
@Override
public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float
velocityY) {
Log.d("GestureDetector","onFling");
return true;
}
@Override
public void onLongPress(MotionEvent event) {
GoalKicker.com – Android™ Notes for Professionals 778
Log.d("GestureDetector","onLongPress");
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.d("GestureDetector","onScroll");
return true;
}
@Override
public void onShowPress(MotionEvent event) {
Log.d("GestureDetector","onShowPress");
}
@Override
public boolean onSingleTapUp(MotionEvent event) {
Log.d("GestureDetector","onSingleTapUp");
return true;
}
@Override
public boolean onDoubleTap(MotionEvent event) {
Log.d("GestureDetector","onDoubleTap");
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent event) {
Log.d("GestureDetector","onDoubleTapEvent");
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent event) {
Log.d("GestureDetector","onSingleTapConfirmed");
return true;
}
}
GoalKicker.com – Android™ Notes for Professionals 779
Chapter 139: Doze Mode
Section 139.1: Whitelisting an Android application
programmatically
Whitelisting won't disable the doze mode for your app, but you can do that by using network and hold-wake locks.
Whitelisting an Android application programmatically can be done as follows:
boolean isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(getPackageName());
if(!isIgnoringBatteryOptimizations){
Intent intent = new Intent();
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, MY_IGNORE_OPTIMIZATION_REQUEST);
}
The result of starting the activity above can be verfied by the following code:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == MY_IGNORE_OPTIMIZATION_REQUEST) {
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
boolean isIgnoringBatteryOptimizations =
pm.isIgnoringBatteryOptimizations(getPackageName());
if(isIgnoringBatteryOptimizations){
// Ignoring battery optimization
}else{
// Not ignoring battery optimization
}
}
}
Section 139.2: Exclude app from using doze mode
1. Open phone's settings
2. open battery
3. open menu and select "battery optimization"
4. from the dropdown menu select "all apps"
5. select the app you want to whitelist
6. select "don't optimize"
Now this app will show under not optimized apps.
An app can check whether it's whitelisted by calling isIgnoringBatteryOptimizations()
GoalKicker.com – Android™ Notes for Professionals 780
Chapter 140: Colors
Section 140.1: Color Manipulation
To manipulate colors we will modify the argb (Alpha, Red, Green and Blue) values of a color.
First extract RGB values from your color.
int yourColor = Color.parse("#ae1f67");
int red = Color.red(yourColor);
int green = Color.green(yourColor);
int blue = Color.blue(yourColor);
Now you can reduce or increase red, green, and blue values and combine them to be a color again:
int newColor = Color.rgb(red, green, blue);
Or if you want to add some alpha to it, you can add it while creating the color:
int newColor = Color.argb(alpha, red, green, blue);
Alpha and RGB values should be in the range [0-225].
GoalKicker.com – Android™ Notes for Professionals 781
Chapter 141: Keyboard
Section 141.1: Register a callback for keyboard open and close
The idea is to measure a layout before and after each change and if there is a significant change you can be
somewhat certain that its the softkeyboard.
// A variable to hold the last content layout hight
private int mLastContentHeight = 0;
private ViewTreeObserver.OnGlobalLayoutListener keyboardLayoutListener = new
ViewTreeObserver.OnGlobalLayoutListener() {
@Override public void onGlobalLayout() {
int currentContentHeight = findViewById(Window.ID_ANDROID_CONTENT).getHeight();
if (mLastContentHeight > currentContentHeight + 100) {
Timber.d("onGlobalLayout: Keyboard is open");
mLastContentHeight = currentContentHeight;
} else if (currentContentHeight > mLastContentHeight + 100) {
Timber.d("onGlobalLayout: Keyboard is closed");
mLastContentHeight = currentContentHeight;
}
}
};
then in our onCreate set the initial value for mLastContentHeight
mLastContentHeight = findViewById(Window.ID_ANDROID_CONTENT).getHeight();
and add the listener
rootView.getViewTreeObserver().addOnGlobalLayoutListener(keyboardLayoutListener);
don't forget to remove the listener on destroy
rootView.getViewTreeObserver().removeOnGlobalLayoutListener(keyboardLayoutListener);
Section 141.2: Hide keyboard when user taps anywhere else
on the screen
Add code in your Activity.
This would work for Fragment also, no need to add this code in Fragment.
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
View view = getCurrentFocus();
if (view != null && (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() ==
MotionEvent.ACTION_MOVE) && view instanceof EditText &&
!view.getClass().getName().startsWith("android.webkit.")) {
int scrcoords[] = new int[2];
view.getLocationOnScreen(scrcoords);
float x = ev.getRawX() + view.getLeft() - scrcoords[0];
float y = ev.getRawY() + view.getTop() - scrcoords[1];
if (x < view.getLeft() || x > view.getRight() || y < view.getTop() || y > view.getBottom())
GoalKicker.com – Android™ Notes for Professionals 782
((InputMethodManager)this.getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow((
this.getWindow().getDecorView().getApplicationWindowToken()), 0);
}
return super.dispatchTouchEvent(ev);
}
GoalKicker.com – Android™ Notes for Professionals 783
Chapter 142: RenderScript
RenderScript is a scripting language that allows you to write high performance graphic rendering and raw
computational code. It provides a means of writing performance critical code that the system later compiles to
native code for the processor it can run on. This could be the CPU, a multi-core CPU, or even the GPU. Which it
ultimately runs on depends on many factors that aren't readily available to the developer, but also depends on
what architecture the internal platform compiler supports.
Section 142.1: Getting Started
RenderScript is a framework to allow high performance parallel computation on Android. Scripts you write will be
executed across all available processors (e.g. CPU, GPU etc) in parallel allowing you to focus on the task you want to
achieve instead of how it is scheduled and executed.
Scripts are written in a C99 based language (C99 being an old version of the C programming language standard).
For each Script a Java class is created which allows you to easily interact with RenderScript in your Java code.
Setting up your project
There exist two different ways to access RenderScript in your app, with the Android Framework libraries or the
Support Library. Even if you don't want to target devices before API level 11 you should always use the Support
Library implementation because it ensures devices compatibility across many different devices. To use the support
library implementation you need to use at least build tools version 18.1.0!
Now lets setup the build.gradle file of your application:
android {
compileSdkVersion 24
buildToolsVersion '24.0.1'
defaultConfig {
minSdkVersion 8
targetSdkVersion 24
renderscriptTargetApi 18
renderscriptSupportModeEnabled true
}
}
renderscriptTargetApi: This should be set to the version earliest API level which provides all RenderScript
functionality you require.
renderscriptSupportModeEnabled: This enables the use of the Support Library RenderScript
implementation.
How RenderScript works
A typical RenderScript consists of two things: Kernels and Functions. A function is just what it sounds like - it accepts
an input, does something with that input and returns an output. A Kernel is where the real power of RenderScript
comes from.
A Kernel is a function which is executed against every element inside an Allocation. An Allocation can be used to
pass data like a Bitmap or a byte array to a RenderScript and they are also used to get a result from a Kernel.
Kernels can either take one Allocation as input and another as output or they can modify the data inside just one
Allocation.
GoalKicker.com – Android™ Notes for Professionals 784
You can write your one Kernels, but there are also many predefined Kernels which you can use to perform common
operations like a Gaussian Image Blur.
As already mentioned for every RenderScript file a class is generated to interact with it. These classes always start
with the prefix ScriptC_ followed by the name of the RenderScript file. For example if your RenderScript file is
called example then the generated Java class will be called ScriptC_example. All predefined Scripts just start with
the prefix Script - for example the Gaussian Image Blur Script is called ScriptIntrinsicBlur.
Writing your first RenderScript
The following example is based on an example on GitHub. It performs basic image manipulation by modifying the
saturation of an image. You can find the source code here and check it out if you want to play around with it
yourself. Here's a quick gif of what the result is supposed to look like:
RenderScript Boilerplate
RenderScript files reside in the folder src/main/rs in your project. Each file has the file extension .rs and has to
contain two #pragma statements at the top:
#pragma version(1)
#pragma rs java_package_name(your.package.name)
#pragma version(1): This can be used to set the version of RenderScript you are using. Currently there is
only version 1.
#pragma rs java_package_name(your.package.name): This can be used to set the package name of the Java
class generated to interact with this particular RenderScript.
There is another #pragma you should usually set in each of your RenderScript files and it is used to set the floating
GoalKicker.com – Android™ Notes for Professionals 785
point precision. You can set the floating point precision to three different levels:
#pragma rs_fp_full: This is the strictest setting with the highest precision and it is also the default value if
don't specify anything. You should use this if you require high floating point precision.
#pragma rs_fp_relaxed: This is ensures not quite as high floating point precision, but on some architectures
it enables a bunch of optimizations which can cause your scripts to run faster.
#pragma rs_fp_imprecise: This ensures even less precision and should be used if floating point precision
does not really matter to your script.
Most scripts can just use #pragma rs_fp_relaxed unless you really need high floating point precision.
Global Variables
Now just like in C code you can define global variables or constants:
const static float3 gMonoMult = {0.299f, 0.587f, 0.114f};
float saturationLevel = 0.0f;
The variable gMonoMult is of type float3. This means it is a vector consisting of 3 float numbers. The other float
variable called saturationValue is not constant, therefore you can set it at runtime to a value you like. You can use
variables like this in your Kernels or functions and therefore they are another way to give input to or receive output
from your RenderScripts. For each not constant variable a getter and setter method will be generated on the
associated Java class.
Kernels
But now lets get started implementing the Kernel. For the purposes of this example I am not going to explain the
math used in the Kernel to modify the saturation of the image, but instead will focus on how to implement a Kernel
and and how to use it. At the end of this chapter I will quickly explain what the code in this Kernel is actually doing.
Kernels in general
Let's take a look at the source code first:
uchar4 __attribute__((kernel)) saturation(uchar4 in) {
float4 f4 = rsUnpackColor8888(in);
float3 dotVector = dot(f4.rgb, gMonoMult);
float3 newColor = mix(dotVector, f4.rgb, saturationLevel);
return rsPackColorTo8888(newColor);
}
As you can see it looks like a normal C function with one exception: The __attribute__((kernel)) between the
return type and method name. This is what tells RenderScript that this method is a Kernel. Another thing you might
notice is that this method accepts a uchar4 parameter and returns another uchar4 value. uchar4 is - like the float3
variable we discussed in the chapter before - a vector. It contains 4 uchar values which are just byte values in the
range from 0 to 255.
You can access these individual values in many different ways, for example in.r would return the byte which
corresponds to the red channel of a pixel. We use a uchar4 since each pixel is made up of 4 values - r for red, g for
green, b for blue and a for alpha - and you can access them with this shorthand. RenderScript also allows you to
take any number of values from a vector and create another vector with them. For example in.rgb would return a
uchar3 value which just contains the red, green and blue parts of the pixel without the alpha value.
At runtime RenderScript will call this Kernel method for each pixel of an image which is why the return value and
GoalKicker.com – Android™ Notes for Professionals 786
parameter are just one uchar4 value. RenderScript will run many of these calls in parallel on all available processors
which is why RenderScript is so powerful. This also means that you don't have to worry about threading or thread
safety, you can just implement whatever you want to do to each pixel and RenderScript takes care of the rest.
When calling a Kernel in Java you supply two Allocation variables, one which contains the input data and another
one which will receive the output. Your Kernel method will be called for each value in the input Allocation and will
write the result to the output Allocation.
RenderScript Runtime API methods
In the Kernel above a few methods are used which are provided out of the box. RenderScript provides many such
methods and they are vital for almost anything you are going to do with RenderScript. Among them are methods to
do math operations like sin() and helper methods like mix() which mixes two values according to another values.
But there are also methods for more complex operations when dealing with vectors, quaternions and matrices.
The official RenderScript Runtime API Reference is the best resource out there if you want to know more about a
particular method or are looking for a specific method which performs a common operation like calculating the dot
product of a matrix. You can find this documentation here.
Kernel Implementation
Now let's take a look at the specifics of what this Kernel is doing. Here's the first line in the Kernel:
float4 f4 = rsUnpackColor8888(in);
The first line calls the built in method rsUnpackColor8888() which transforms the uchar4 value to a float4 values.
Each color channel is also transformed to the range 0.0f - 1.0f where 0.0f corresponds to a byte value of 0 and
1.0f to 255. The main purpose of this is to make all the math in this Kernel a lot simpler.
float3 dotVector = dot(f4.rgb, gMonoMult);
This next line uses the built in method dot() to calculate the dot product of two vectors. gMonoMult is a constant
value we defined a few chapters above. Since both vectors need to be of the same length to calculate the dot
product and also since we just want to affect the color channels and not the alpha channel of a pixel we use the
shorthand .rgb to get a new float3 vector which just contains the red, green and blue color channels. Those of us
who still remember from school how the dot product works will quickly notice that the dot product should return
just one value and not a vector. Yet in the code above we are assigning the result to a float3 vector. This is again a
feature of RenderScript. When you assign a one dimensional number to a vector all elements in the vector will be
set to this value. For example the following snippet will assign 2.0f to each of the three values in the float3 vector:
float3 example = 2.0f;
So the result of the dot product above is assigned to each element in the float3 vector above.
Now comes the part in which we actually use the global variable saturationLevel to modify the saturation of the
image:
float3 newColor = mix(dotVector, f4.rgb, saturationLevel);
This uses the built in method mix() to mix together the original color with the dot product vector we created above.
How they are mixed together is determined by the global saturationLevel variable. So a saturationLevel of 0.0f
will cause the resulting color to have no part of the original color values and will only consist of values in the
dotVector which results in a black and white or grayed out image. A value of 1.0f will cause the resulting color to
GoalKicker.com – Android™ Notes for Professionals 787
be completely made up of the original color values and values above 1.0f will multiply the original colors to make
them more bright and intense.
return rsPackColorTo8888(newColor);
This is the last part in the Kernel. rsPackColorTo8888() transforms the float3 vector back to a uchar4 value which
is then returned. The resulting byte values are clamped to a range between 0 and 255, so float values higher than
1.0f will result in a byte value of 255 and values lower than 0.0 will result in a byte value of 0.
And that is the whole Kernel implementation. Now there is only one part remaining: How to call a Kernel in Java.
Calling RenderScript in Java
Basics
As was already mentioned above for each RenderScript file a Java class is generated which allows you to interact
with the your scripts. These files have the prefix ScriptC_ followed by the name of the RenderScript file. To create
an instance of these classes you first need an instance of the RenderScript class:
final RenderScript renderScript = RenderScript.create(context);
The static method create() can be used to create a RenderScript instance from a Context. You can then
instantiate the Java class which was generated for your script. If you called the RenderScript file saturation.rs then
the class will be called ScriptC_saturation:
final ScriptC_saturation script = new ScriptC_saturation(renderScript);
On this class you can now set the saturation level and call the Kernel. The setter which was generated for the
saturationLevel variable will have the prefix set_ followed by the name of the variable:
script.set_saturationLevel(1.0f);
There is also a getter prefixed with get_ which allows you to get the saturation level currently set:
float saturationLevel = script.get_saturationLevel();
Kernels you define in your RenderScript are prefixed with forEach_ followed by the name of the Kernel method.
The Kernel we have written expects an input Allocation and an output Allocation as its parameters:
script.forEach_saturation(inputAllocation, outputAllocation);
The input Allocation needs to contain the input image, and after the forEach_saturation method has finished
the output allocation will contain the modified image data.
Once you have an Allocation instance you can copy data from and to those Allocations by using the methods
copyFrom() and copyTo(). For example you can copy a new image into your input `Allocation by calling:
inputAllocation.copyFrom(inputBitmap);
The same way you can retrieve the result image by calling copyTo() on the output Allocation:
outputAllocation.copyTo(outputBitmap);
Creating Allocation instances
GoalKicker.com – Android™ Notes for Professionals 788
There are many ways to create an Allocation. Once you have an Allocation instance you can copy new data from
and to those Allocations with copyTo() and copyFrom() like explained above, but to create them initially you have
to know with what kind of data you are exactly working with. Let's start with the input Allocation:
We can use the static method createFromBitmap() to quickly create our input Allocation from a Bitmap:
final Allocation inputAllocation = Allocation.createFromBitmap(renderScript, image);
In this example the input image never changes so we never need to modify the input Allocation again. We can
reuse it each time the saturationLevel changes to create a new output Bitmap.
Creating the output Allocation is a little more complex. First we need to create what's called a Type. A Type is used
to tell an Allocation with what kind of data it's dealing with. Usually one uses the Type.Builder class to quickly
create an appropriate Type. Let's take a look at the code first:
final Type outputType = new Type.Builder(renderScript, Element.RGBA_8888(renderScript))
.setX(inputBitmap.getWidth())
.setY(inputBitmap.getHeight())
.create();
We are working with a normal 32 bit (or in other words 4 byte) per pixel Bitmap with 4 color channels. That's why
we are choosing Element.RGBA_8888 to create the Type. Then we use the methods setX() and setY() to set the
width and height of the output image to the same size as the input image. The method create() then creates the
Type with the parameters we specified.
Once we have the correct Type we can create the output Allocation with the static method createTyped():
final Allocation outputAllocation = Allocation.createTyped(renderScript, outputType);
Now we are almost done. We also need an output Bitmap in which we can copy the data from the output
Allocation. To do this we use the static method createBitmap() to create a new empty Bitmap with the same size
and configuration as the input Bitmap.
final Bitmap outputBitmap = Bitmap.createBitmap(
inputBitmap.getWidth(),
inputBitmap.getHeight(),
inputBitmap.getConfig()
);
And with that we have all the puzzle pieces to execute our RenderScript.
Full example
Now let's put all this together in one example:
// Create the RenderScript instance
final RenderScript renderScript = RenderScript.create(context);
// Create the input Allocation
final Allocation inputAllocation = Allocation.createFromBitmap(renderScript, inputBitmap);
// Create the output Type.
final Type outputType = new Type.Builder(renderScript, Element.RGBA_8888(renderScript))
.setX(inputBitmap.getWidth())
.setY(inputBitmap.getHeight())
.create();
GoalKicker.com – Android™ Notes for Professionals 789
// And use the Type to create am output Allocation
final Allocation outputAllocation = Allocation.createTyped(renderScript, outputType);
// Create an empty output Bitmap from the input Bitmap
final Bitmap outputBitmap = Bitmap.createBitmap(
inputBitmap.getWidth(),
inputBitmap.getHeight(),
inputBitmap.getConfig()
);
// Create an instance of our script
final ScriptC_saturation script = new ScriptC_saturation(renderScript);
// Set the saturation level
script.set_saturationLevel(2.0f);
// Execute the Kernel
script.forEach_saturation(inputAllocation, outputAllocation);
// Copy the result data to the output Bitmap
outputAllocation.copyTo(outputBitmap);
// Display the result Bitmap somewhere
someImageView.setImageBitmap(outputBitmap);
Conclusion
With this introduction you should be all set to write your own RenderScript Kernels for simple image manipulation.
However there are a few things you have to keep in mind:
RenderScript only works in Application projects: Currently RenderScript files cannot be part of a library
project.
Watch out for memory: RenderScript is very fast, but it can also be memory intensive. There should never
be more than one instance of RenderScript at any time. You should also reuse as much as possible.
Normally you just need to create your Allocation instances once and can reuse them in the future. The
same goes for output Bitmaps or your script instances. Reuse as much as possible.
Do your work in the background: Again RenderScript is very fast, but not instant in any way. Any Kernel,
especially complex ones should be executed off the UI thread in an AsyncTask or something similar. However
for the most part you don't have to worry about memory leaks. All RenderScript related classes only use the
application Context and therefore don't cause memory leaks. But you still have to worry about the usual
stuff like leaking View, Activity or any Context instance which you use yourself!
Use built in stuff: There are many predefined scripts which perform tasks like image blurring, blending,
converting, resizing. And there are many more built in methods which help you implement your kernels.
Chances are that if you want to do something there is either a script or method which already does what you
are trying to do. Don't reinvent the wheel.
If you want to quickly get started and play around with actual code I recommend you take a look at the example
GitHub project which implements the exact example talked about in this tutorial. You can find the project here.
Have fun with RenderScript!
Section 142.2: Blur a View
BlurBitmapTask.java
public class BlurBitmapTask extends AsyncTask<Bitmap, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private final RenderScript renderScript;
GoalKicker.com – Android™ Notes for Professionals 790
private boolean shouldRecycleSource = false;
public BlurBitmapTask(@NonNull Context context, @NonNull ImageView imageView) {
// Use a WeakReference to ensure
// the ImageView can be garbage collected
imageViewReference = new WeakReference<>(imageView);
renderScript = RenderScript.create(context);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Bitmap... params) {
Bitmap bitmap = params[0];
return blurBitmap(bitmap);
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap == null || isCancelled()) {
return;
}
final ImageView imageView = imageViewReference.get();
if (imageView == null) {
return;
}
imageView.setImageBitmap(bitmap);
}
public Bitmap blurBitmap(Bitmap bitmap) {
// https://plus.google.com/+MarioViviani/posts/fhuzYkji9zz
//Let's create an empty bitmap with the same size of the bitmap we want to blur
Bitmap outBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
Bitmap.Config.ARGB_8888);
//Instantiate a new Renderscript
//Create an Intrinsic Blur Script using the Renderscript
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(renderScript,
Element.U8_4(renderScript));
//Create the in/out Allocations with the Renderscript and the in/out bitmaps
Allocation allIn = Allocation.createFromBitmap(renderScript, bitmap);
Allocation allOut = Allocation.createFromBitmap(renderScript, outBitmap);
//Set the radius of the blur
blurScript.setRadius(25.f);
//Perform the Renderscript
blurScript.setInput(allIn);
blurScript.forEach(allOut);
//Copy the final bitmap created by the out Allocation to the outBitmap
allOut.copyTo(outBitmap);
// recycle the original bitmap
// nope, we are using the original bitmap as well :/
if (shouldRecycleSource) {
GoalKicker.com – Android™ Notes for Professionals 791
bitmap.recycle();
}
//After finishing everything, we destroy the Renderscript.
renderScript.destroy();
return outBitmap;
}
public boolean isShouldRecycleSource() {
return shouldRecycleSource;
}
public void setShouldRecycleSource(boolean shouldRecycleSource) {
this.shouldRecycleSource = shouldRecycleSource;
}
}
Usage:
ImageView imageViewOverlayOnViewToBeBlurred
.setImageDrawable(ContextCompat.getDrawable(this, android.R.color.transparent));
View viewToBeBlurred.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_LOW);
viewToBeBlurred.setDrawingCacheEnabled(true);
BlurBitmapTask blurBitmapTask = new BlurBitmapTask(this, imageViewOverlayOnViewToBeBlurred);
blurBitmapTask.execute(Bitmap.createBitmap(viewToBeBlurred.getDrawingCache()));
viewToBeBlurred.setDrawingCacheEnabled(false);
Section 142.3: Blur an image
This example demonstrates how to use Renderscript API to blur an image (using Bitmap). This example uses
ScriptInstrinsicBlur provided by android Renderscript API (API >= 17).
public class BlurProcessor {
private RenderScript rs;
private Allocation inAllocation;
private Allocation outAllocation;
private int width;
private int height;
private ScriptIntrinsicBlur blurScript;
public BlurProcessor(RenderScript rs) {
this.rs = rs;
}
public void initialize(int width, int height) {
blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
blurScript.setRadius(7f); // Set blur radius. 25 is max
if (outAllocation != null) {
outAllocation.destroy();
outAllocation = null;
}
// Bitmap must have ARGB_8888 config for this type
Type bitmapType = new Type.Builder(rs, Element.RGBA_8888(rs))
.setX(width)
.setY(height)
.setMipmaps(false) // We are using MipmapControl.MIPMAP_NONE
GoalKicker.com – Android™ Notes for Professionals 792
.create();
// Create output allocation
outAllocation = Allocation.createTyped(rs, bitmapType);
// Create input allocation with same type as output allocation
inAllocation = Allocation.createTyped(rs, bitmapType);
}
public void release() {
if (blurScript != null) {
blurScript.destroy();
blurScript = null;
}
if (inAllocation != null) {
inAllocation.destroy();
inAllocation = null;
}
if (outAllocation != null) {
outAllocation.destroy();
outAllocation = null;
}
}
public Bitmap process(Bitmap bitmap, boolean createNewBitmap) {
if (bitmap.getWidth() != width || bitmap.getHeight() != height) {
// Throw error if required
return null;
}
// Copy data from bitmap to input allocations
inAllocation.copyFrom(bitmap);
// Set input for blur script
blurScript.setInput(inAllocation);
// process and set data to the output allocation
blurScript.forEach(outAllocation);
if (createNewBitmap) {
Bitmap returnVal = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
outAllocation.copyTo(returnVal);
return returnVal;
}
outAllocation.copyTo(bitmap);
return bitmap;
}
}
Each script has a kernel which processes the data and it is generally invoked via forEach method.
public class BlurActivity extends AppCompatActivity {
private BlurProcessor blurProcessor;
@Override
public void onCreate(Bundle savedInstanceState) {
// setup layout and other stuff
GoalKicker.com – Android™ Notes for Professionals 793
blurProcessor = new BlurProcessor(Renderscript.create(getApplicationContext()));
}
private void loadImage(String path) {
// Load image to bitmap
Bitmap bitmap = loadBitmapFromPath(path);
// Initialize processor for this bitmap
blurProcessor.release();
blurProcessor.initialize(bitmap.getWidth(), bitmap.getHeight());
// Blur image
Bitmap blurImage = blurProcessor.process(bitmap, true); // Use newBitamp as false if you
don't want to create a new bitmap
}
}
This concluded the example here. It is advised to do the processing in a background thread.
GoalKicker.com – Android™ Notes for Professionals 794
Chapter 143: Fresco
Fresco is a powerful system for displaying images in Android applications.
In Android 4.x and lower, Fresco puts images in a special region of Android memory (called ashmem). This lets
your application run faster - and suffer the dreaded OutOfMemoryError much less often.
Fresco also supports streaming of JPEGs.
Section 143.1: Getting Started with Fresco
First, add Fresco to your build.gradle as shown in the Remarks section:
If you need additional features, like animated GIF or WebP support, you have to add the corresponding Fresco
artifacts as well.
Fresco needs to be initialized. You should only do this 1 time, so placing the initialization in your Application is a
good idea. An example for this would be:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Fresco.initialize(this);
}
}
If you want to load remote images from a server, your app needs the internt permission. Simply add it to your
AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
Then, add a SimpleDraweeView to your XML layout. Fresco does not support wrap_content for image dimensions
since you might have multiple images with different dimensions (placeholder image, error image, actual image, ...).
So you can either add a SimpleDraweeView with fixed dimensions (or match_parent):
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/my_image_view"
android:layout_width="120dp"
android:layout_height="120dp"
fresco:placeholderImage="@drawable/placeholder" />
Or supply an aspect ratio for your image:
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/my_image_view"
android:layout_width="120dp"
android:layout_height="wrap_content"
fresco:viewAspectRatio="1.33"
fresco:placeholderImage="@drawable/placeholder" />
Finally, you can set your image URI in Java:
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
GoalKicker.com – Android™ Notes for Professionals 795
draweeView.setImageURI("http://yourdomain.com/yourimage.jpg");
That's it! You should see your placeholder drawable until the network image has been fetched.
Section 143.2: Using OkHttp 3 with Fresco
First, in addition to the normal Fresco Gradle dependency, you have to add the OkHttp 3 dependency to your
build.gradle:
compile "com.facebook.fresco:imagepipeline-okhttp3:1.2.0" // Or a newer version.
When you initialize Fresco (usually in your custom Application implementation), you can now specify your OkHttp
client:
OkHttpClient okHttpClient = new OkHttpClient(); // Build on your own OkHttpClient.
Context context = ... // Your Application context.
ImagePipelineConfig config = OkHttpImagePipelineConfigFactory
.newBuilder(context, okHttpClient)
.build();
Fresco.initialize(context, config);
Section 143.3: JPEG Streaming with Fresco using
DraweeController
This example assumes that you have already added Fresco to your app (see this example):
SimpleDraweeView img = new SimpleDraweeView(context);
ImageRequest request = ImageRequestBuilder
.newBuilderWithSource(Uri.parse("http://example.com/image.png"))
.setProgressiveRenderingEnabled(true) // This is where the magic happens.
.build();
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setImageRequest(request)
.setOldController(img.getController()) // Get the current controller from our
SimpleDraweeView.
.build();
img.setController(controller); // Set the new controller to the SimpleDraweeView to enable
progressive JPEGs.
GoalKicker.com – Android™ Notes for Professionals 796
Chapter 144: Swipe to Refresh
Section 144.1: How to add Swipe-to-Refresh To your app
Make sure the following dependency is added to your app's build.gradle file under dependencies:
compile 'com.android.support:support-core-ui:24.2.0'
Then add the SwipeRefreshLayout in your layout:
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- place your view here -->
</android.support.v4.widget.SwipeRefreshLayout>
Finally implement the SwipeRefreshLayout.OnRefreshListener listener.
mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout);
mSwipeRefreshLayout.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh() {
// your code
}
});
Section 144.2: Swipe To Refresh with RecyclerView
To add a Swipe To Refresh layout with a RecyclerView add the following to your Activity/Fragment layout file:
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:scrollbars="vertical" />
</android.support.v4.widget.SwipeRefreshLayout>
In your Activity/Fragment add the following to initialize the SwipeToRefreshLayout:
SwipeRefreshLayout mSwipeRefreshLayout = (SwipeRefreshLayout)
findViewById(R.id.refresh_layout);
mSwipeRefreshLayout.setColorSchemeResources(R.color.green_bg,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light);
GoalKicker.com – Android™ Notes for Professionals 797
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
// Execute code when refresh layout swiped
}
});
GoalKicker.com – Android™ Notes for Professionals 798
Chapter 145: Creating Splash screen
Section 145.1: Splash screen with animation
This example shows a simple but effective splash screen with animation that can be created by using Android
Studio.
Step 1: Create an animation
Create a new directory named anim in the res directory. Right-click it and create a new Animation Resource file
named fade_in.xml:
Then, put the following code into the fade_in.xml file:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true" >
<alpha
android:duration="1000"
android:fromAlpha="0.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toAlpha="1.0" />
</set>
Step 2: Create an activity
Create an empty activity using Android Studio named Splash. Then, put the following code into it:
public class Splash extends AppCompatActivity {
Animation anim;
ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
imageView=(ImageView)findViewById(R.id.imageView2); // Declare an imageView to show the
animation.
anim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.fade_in); // Create the
animation.
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
startActivity(new Intent(this,HomeActivity.class));
// HomeActivity.class is the activity to go after showing the splash screen.
}
GoalKicker.com – Android™ Notes for Professionals 799
@Override
public void onAnimationRepeat(Animation animation) {
}
});
imageView.startAnimation(anim);
}
}
Next, put the following code into the layout file:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_splash"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="your_packagename"
android:orientation="vertical"
android:background="@android:color/white">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/imageView2"
android:layout_weight="1"
android:src="@drawable/Your_logo_or_image" />
</LinearLayout>
Step 3: Replace the default launcher
Turn your Splash activity into a launcher by adding the following code to the AndroidManifest file:
<activity
android:name=".Splash"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Then, remove the default launcher activity by removing the following code from the AndroidManifest file:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
Section 145.2: A basic splash screen
A splash screen is just like any other activity, but it can handle all of your startup-needs in the background. Example:
Manifest:
GoalKicker.com – Android™ Notes for Professionals 800
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.package"
android:versionCode="1"
android:versionName="1.0" >
<application
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".Splash"
android:label="@string/app_name"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Now our splash-screen will be called as the first activity.
Here is an example splashscreen that also handles some critical app elements:
public class Splash extends Activity{
public final int SPLASH_DISPLAY_LENGTH = 3000;
private void checkPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WAKE_LOCK) !=
PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this,Manifest.permission.INTERNET) !=
PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_NETWORK_STATE)
!= PackageManager.PERMISSION_GRANTED) {//Can add more as per requirement
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WAKE_LOCK,
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE},
123);
}
}
@Override
protected void onCreate(Bundle sis){
super.onCreate(sis);
//set the content view. The XML file can contain nothing but an image, such as a logo or the
app icon
setContentView(R.layout.splash);
//we want to display the splash screen for a few seconds before it automatically
GoalKicker.com – Android™ Notes for Professionals 801
//disappears and loads the game. So we create a thread:
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//request permissions. NOTE: Copying this and the manifest will cause the app to
crash as the permissions requested aren't defined in the manifest.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) {
checkPermission();
}
String lang = [load or determine the system language and set to default if it
isn't available.]
Locale locale = new Locale(lang);
Locale.setDefault(locale);
Configuration config = new Configuration ();
config.locale = locale;
Splash.this.getResources().updateConfiguration(config,
Splash.this.getResources().getDisplayMetrics()) ;
//after three seconds, it will execute all of this code.
//as such, we then want to redirect to the master-activity
Intent mainIntent = new Intent(Splash.this, MainActivity.class);
Splash.this.startActivity(mainIntent);
//then we finish this class. Dispose of it as it is longer needed
Splash.this.finish();
}
}, SPLASH_DISPLAY_LENGTH);
}
public void onPause(){
super.onPause();
finish();
}
}
GoalKicker.com – Android™ Notes for Professionals 802
Chapter 146: IntentService
Section 146.1: Creating an IntentService
To create an IntentService, create a class which extends IntentService, and within it, a method which overrides
onHandleIntent:
package com.example.myapp;
public class MyIntentService extends IntentService {
@Override
protected void onHandleIntent (Intent workIntent) {
//Do something in the background, based on the contents of workIntent.
}
}
Section 146.2: Basic IntentService Example
The abstract class IntentService is a base class for services, which run in the background without any user
interface. Therefore, in order to update the UI, we have to make use of a receiver, which may be either a
BroadcastReceiver or a ResultReceiver:
A BroadcastReceiver should be used if your service needs to communicate with multiple components that
want to listen for communication.
A ResultReceiver: should be used if your service needs to communicate with only the parent application (i.e.
your application).
Within the IntentService, we have one key method, onHandleIntent(), in which we will do all actions, for
example, preparing notifications, creating alarms, etc.
If you want to use you own IntentService, you have to extend it as follows:
public class YourIntentService extends IntentService {
public YourIntentService () {
super("YourIntentService ");
}
@Override
protected void onHandleIntent(Intent intent) {
// TODO: Write your own code here.
}
}
Calling/starting the activity can be done as follows:
Intent i = new Intent(this, YourIntentService.class);
startService(i); // For the service.
startActivity(i); // For the activity; ignore this for now.
Similar to any activity, you can pass extra information such as bundle data to it as follows:
Intent passDataIntent = new Intent(this, YourIntentService.class);
msgIntent.putExtra("foo","bar");
startService(passDataIntent);
Now assume that we passed some data to the YourIntentService class. Based on this data, an action can be
GoalKicker.com – Android™ Notes for Professionals 803
performed as follows:
public class YourIntentService extends IntentService {
private String actvityValue="bar";
String retrivedValue=intent.getStringExtra("foo");
public YourIntentService () {
super("YourIntentService ");
}
@Override
protected void onHandleIntent(Intent intent) {
if(retrivedValue.equals(actvityValue)){
// Send the notification to foo.
} else {
// Retrieving data failed.
}
}
}
The code above also shows how to handle constraints in the OnHandleIntent() method.
Section 146.3: Sample Intent Service
Here is an example of an IntentService that pretends to load images in the background. All you need to do to
implement an IntentService is to provide a constructor that calls the super(String) constructor, and you need to
implement the onHandleIntent(Intent) method.
public class ImageLoaderIntentService extends IntentService {
public static final String IMAGE_URL = "url";
/**
* Define a constructor and call the super(String) constructor, in order to name the worker
* thread - this is important if you want to debug and know the name of the thread upon
* which this Service is operating its jobs.
*/
public ImageLoaderIntentService() {
super("Example");
}
@Override
protected void onHandleIntent(Intent intent) {
// This is where you do all your logic - this code is executed on a background thread
String imageUrl = intent.getStringExtra(IMAGE_URL);
if (!TextUtils.isEmpty(imageUrl)) {
Drawable image = HttpUtils.loadImage(imageUrl); // HttpUtils is made-up for the example
}
// Send your drawable back to the UI now, so that you can use it - there are many ways
// to achieve this, but they are out of reach for this example
}
}
In order to start an IntentService, you need to send an Intent to it. You can do so from an Activity, for an
example. Of course, you're not limited to that. Here is an example of how you would summon your new Service
from an Activity class.
GoalKicker.com – Android™ Notes for Professionals 804
Intent serviceIntent = new Intent(this, ImageLoaderIntentService.class); // you can use 'this' as
the first parameter if your class is a Context (i.e. an Activity, another Service, etc.), otherwise,
supply the context differently
serviceIntent.putExtra(IMAGE_URL, "http://www.example-site.org/some/path/to/an/image");
startService(serviceIntent); // if you are not using 'this' in the first line, you also have to put
the call to the Context object before startService(Intent) here
The IntentService processes the data from its Intents sequentially, so that you can send multiple Intents without
worrying whether they will collide with each other. Only one Intent at a time is processed, the rest go in a queue.
When all the jobs are complete, the IntentService will shut itself down automatically.
GoalKicker.com – Android™ Notes for Professionals 805
Chapter 147: Implicit Intents
Parameters Details
o Intent
action String: The Intent action, such as ACTION_VIEW.
uri Uri: The Intent data URI.
packageContext Context: A Context of the application package implementing this class.
cls Class: The component class that is to be used for the intent.
Section 147.1: Implicit and Explicit Intents
An explicit intent is used for starting an activity or service within the same application package. In this case the
name of the intended class is explicitly mentioned:
Intent intent = new Intent(this, MyComponent.class);
startActivity(intent);
However, an implicit intent is sent across the system for any application installed on the user's device that can
handle that intent. This is used to share information between different applications.
Intent intent = new Intent("com.stackoverflow.example.VIEW");
//We need to check to see if there is an application installed that can handle this intent
if (getPackageManager().resolveActivity(intent, 0) != null){
startActivity(intent);
}else{
//Handle error
}
More details on the differences can be found in the Android Developer docs here: Intent Resolution
Section 147.2: Implicit Intents
Implicit intents do not name a specific component, but instead declare a general action to perform, which allows a
component from another app to handle it.
For example, if you want to show the user a location on a map, you can use an implicit intent to request that
another capable app show a specified location on a map.
Example:
// Create the text message with a string
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
sendIntent.setType("text/plain");
// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
startActivity(sendIntent);
}
GoalKicker.com – Android™ Notes for Professionals 806
Chapter 148: Publish to Play Store
Section 148.1: Minimal app submission guide
Requirements:
A developer account
An apk already built and signed with a non-debug key
A free app that doesn't have in-app billing
no Firebase Cloud Messaging or Game Services
1. Head to https://play.google.com/apps/publish/
1a) Create your developer account if you do not have one
2. Click button Create new Application
3. Click submit APK
4. Fill in all required fields in the form, including some assets that will be displayed on the Play Store (see image
below)
5. When satisfied hit Publish app button
GoalKicker.com – Android™ Notes for Professionals 807
See more about signing in Configure Signing Settings
GoalKicker.com – Android™ Notes for Professionals 808
Chapter 149: Universal Image Loader
Section 149.1: Basic usage
1. Load an image, decode it into a bitmap, and display the bitmap in an ImageView (or any other view which
implements the ImageAware interface):
ImageLoader.getInstance().displayImage(imageUri, imageView);
2. Load an image, decode it into a bitmap, and return the bitmap to a callback:
ImageLoader.getInstance().loadImage(imageUri, new SimpleImageLoadingListener() {
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
// Do whatever you want with the bitmap.
}
});
3. Load an image, decode it into a bitmap and return the bitmap synchronously:
Bitmap bmp = ImageLoader.getInstance().loadImageSync(imageUri);
Section 149.2: Initialize Universal Image Loader
1. Add the following dependency to the build.gradle file:
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
2. Add the following permissions to the AndroidManifest.xml file:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
3. Initialize the Universal Image Loader. This must be done before the first usage:
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this)
// ...
.build();
ImageLoader.getInstance().init(config);
The full configuration options can be found here.
GoalKicker.com – Android™ Notes for Professionals 809
Chapter 150: Image Compression
Section 150.1: How to compress image without size change
Get Compressed Bitmap from Singleton class:
ImageView imageView = (ImageView)findViewById(R.id.imageView);
Bitmap bitmap = ImageUtils.getInstant().getCompressedBitmap("Your_Image_Path_Here");
imageView.setImageBitmap(bitmap);
ImageUtils.java:
public class ImageUtils {
public static ImageUtils mInstant;
public static ImageUtils getInstant(){
if(mInstant==null){
mInstant = new ImageUtils();
}
return mInstant;
}
public Bitmap getCompressedBitmap(String imagePath) {
float maxHeight = 1920.0f;
float maxWidth = 1080.0f;
Bitmap scaledBitmap = null;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bmp = BitmapFactory.decodeFile(imagePath, options);
int actualHeight = options.outHeight;
int actualWidth = options.outWidth;
float imgRatio = (float) actualWidth / (float) actualHeight;
float maxRatio = maxWidth / maxHeight;
if (actualHeight > maxHeight || actualWidth > maxWidth) {
if (imgRatio < maxRatio) {
imgRatio = maxHeight / actualHeight;
actualWidth = (int) (imgRatio * actualWidth);
actualHeight = (int) maxHeight;
} else if (imgRatio > maxRatio) {
imgRatio = maxWidth / actualWidth;
actualHeight = (int) (imgRatio * actualHeight);
actualWidth = (int) maxWidth;
} else {
actualHeight = (int) maxHeight;
actualWidth = (int) maxWidth;
}
}
options.inSampleSize = calculateInSampleSize(options, actualWidth, actualHeight);
options.inJustDecodeBounds = false;
options.inDither = false;
options.inPurgeable = true;
options.inInputShareable = true;
options.inTempStorage = new byte[16 * 1024];
GoalKicker.com – Android™ Notes for Professionals 810
try {
bmp = BitmapFactory.decodeFile(imagePath, options);
} catch (OutOfMemoryError exception) {
exception.printStackTrace();
}
try {
scaledBitmap = Bitmap.createBitmap(actualWidth, actualHeight, Bitmap.Config.ARGB_8888);
} catch (OutOfMemoryError exception) {
exception.printStackTrace();
}
float ratioX = actualWidth / (float) options.outWidth;
float ratioY = actualHeight / (float) options.outHeight;
float middleX = actualWidth / 2.0f;
float middleY = actualHeight / 2.0f;
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);
Canvas canvas = new Canvas(scaledBitmap);
canvas.setMatrix(scaleMatrix);
canvas.drawBitmap(bmp, middleX - bmp.getWidth() / 2, middleY - bmp.getHeight() / 2, new
Paint(Paint.FILTER_BITMAP_FLAG));
ExifInterface exif = null;
try {
exif = new ExifInterface(imagePath);
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
Matrix matrix = new Matrix();
if (orientation == 6) {
matrix.postRotate(90);
} else if (orientation == 3) {
matrix.postRotate(180);
} else if (orientation == 8) {
matrix.postRotate(270);
}
scaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(),
scaledBitmap.getHeight(), matrix, true);
} catch (IOException e) {
e.printStackTrace();
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 85, out);
byte[] byteArray = out.toByteArray();
Bitmap updatedBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length);
return updatedBitmap;
}
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
GoalKicker.com – Android™ Notes for Professionals 811
final float totalPixels = width * height;
final float totalReqPixelsCap = reqWidth * reqHeight * 2;
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
inSampleSize++;
}
return inSampleSize;
}
}
Dimensions are same after compressing Bitmap.
How did I checked ?
Bitmap beforeBitmap = BitmapFactory.decodeFile("Your_Image_Path_Here");
Log.i("Before Compress Dimension", beforeBitmap.getWidth()+"-"+beforeBitmap.getHeight());
Bitmap afterBitmap = ImageUtils.getInstant().getCompressedBitmap("Your_Image_Path_Here");
Log.i("After Compress Dimension", afterBitmap.getWidth() + "-" + afterBitmap.getHeight());
Output:
Before Compress : Dimension: 1080-1452
After Compress : Dimension: 1080-1452
GoalKicker.com – Android™ Notes for Professionals 812
Chapter 151: 9-Patch Images
Section 151.1: Basic rounded corners
The key to correctly stretching is in the top and left border.
The top border controls horizontal stretching and the left border controls vertical stretching.
This example creates rounded corners suitable for a Toast.
The parts of the image that are below the top border and to the right of the left border will expand to fill all unused
space.
This example will stretch to all combinations of sizes, as shown below:
Section 151.2: Optional padding lines
Nine-patch images allow optional definition of the padding lines in the image. The padding lines are the lines on the
right and at the bottom.
If a View sets the 9-patch image as its background, the padding lines are used to define the space for the View's
content (e.g. the text input in an EditText). If the padding lines are not defined, the left and top lines are used
instead.
The content area of the stretched image then looks like this:
GoalKicker.com – Android™ Notes for Professionals 813
Section 151.3: Basic spinner
The Spinner can be reskinned according to your own style requirements using a Nine Patch.
As an example, see this Nine Patch:
As you can see, it has 3 extremely small areas of stretching marked.
The top border has only left of the icon marked. That indicates that I want the left side (complete transparency) of
the drawable to fill the Spinner view until the icon is reached.
The left border has marked transparent segments at the top and bottom of the icon marked. That indicates that
both the top and the bottom will expand to the size of the Spinner view. This will leave the icon itself centered
vertically.
Using the image without Nine Patch metadata:
Using the image with Nine Patch metadata:
GoalKicker.com – Android™ Notes for Professionals 814
Chapter 152: Email Validation
Section 152.1: Email address validation
Add the following method to check whether an email address is valid or not:
private boolean isValidEmailId(String email){
return Pattern.compile("^(([\\w-]+\\.)+[\\w-]+|([a-zA-Z]{1}|[\\w-]{2,}))@"
+ "((([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\.([0-1]?"
+ "[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\."
+ "([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\.([0-1]?"
+ "[0-9]{1,2}|25[0-5]|2[0-4][0-9])){1}|"
+ "([a-zA-Z]+[\\w-]+\\.)+[a-zA-Z]{2,4})$").matcher(email).matches();
}
The above method can easily be verified by converting the text of an EditText widget into a String:
if(isValidEmailId(edtEmailId.getText().toString().trim())){
Toast.makeText(getApplicationContext(), "Valid Email Address.", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(getApplicationContext(), "InValid Email Address.", Toast.LENGTH_SHORT).show();
}
Section 152.2: Email Address validation with using Patterns
if (Patterns.EMAIL_ADDRESS.matcher(email).matches()){
Log.i("EmailCheck","It is valid");
}
GoalKicker.com – Android™ Notes for Professionals 815
Chapter 153: Bottom Sheets
A bottom sheet is a sheet that slides up from the bottom edge of the screen.
Section 153.1: Quick Setup
Make sure the following dependency is added to your app's build.gradle file under dependencies:
compile 'com.android.support:design:25.3.1'
Then you can use the Bottom sheet using these options:
BottomSheetBehavior to be used with CoordinatorLayout
BottomSheetDialog which is a dialog with a bottom sheet behavior
BottomSheetDialogFragment which is an extension of DialogFragment, that creates a BottomSheetDialog
instead of a standard dialog.
Section 153.2: BottomSheetBehavior like Google maps
Version ≥ 2.1.x
This example depends on Support Library 23.4.0.+.
BottomSheetBehavior is characterized by :
1. Two toolbars with animations that respond to the bottom sheet movements.
2. A FAB that hides when it is near to the "modal toolbar" (the one that appears when you are sliding up).
3. A backdrop image behind bottom sheet with some kind of parallax effect.
4. A Title (TextView) in Toolbar that appears when bottom sheet reach it.
5. The notification satus bar can turn its background to transparent or full color.
6. A custom bottom sheet behavior with an "anchor" state.
Now let's check them one by one:
ToolBars
When you open that view in Google Maps, you can see a toolbar in where you can search, it's the only one that I'm
not doing exactly like Google Maps, because I wanted to do it more generic. Anyway that ToolBar is inside an
AppBarLayout and it got hidden when you start dragging the BottomSheet and it appears again when the
BottomSheet reach the COLLAPSED state.
To achieve it you need to:
create a Behavior and extend it from AppBarLayout.ScrollingViewBehavior
override layoutDependsOn and onDependentViewChanged methods. Doing it you will listen for bottomSheet
movements.
create some methods to hide and unhide the AppBarLayout/ToolBar with animations.
This is how I did it for first toolbar or ActionBar:
@Override
GoalKicker.com – Android™ Notes for Professionals 816
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof NestedScrollView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
if (mChild == null) {
initValues(child, dependency);
return false;
}
float dVerticalScroll = dependency.getY() - mPreviousY;
mPreviousY = dependency.getY();
//going up
if (dVerticalScroll <= 0 && !hidden) {
dismissAppBar(child);
return true;
}
return false;
}
private void initValues(final View child, View dependency) {
mChild = child;
mInitialY = child.getY();
BottomSheetBehaviorGoogleMapsLike bottomSheetBehavior =
BottomSheetBehaviorGoogleMapsLike.from(dependency);
bottomSheetBehavior.addBottomSheetCallback(new
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet,
@BottomSheetBehaviorGoogleMapsLike.State int newState) {
if (newState == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED ||
newState == BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN)
showAppBar(child);
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
}
});
}
private void dismissAppBar(View child){
hidden = true;
AppBarLayout appBarLayout = (AppBarLayout)child;
mToolbarAnimation =
appBarLayout.animate().setDuration(mContext.getResources().getInteger(android.R.integer.config_shor
tAnimTime));
mToolbarAnimation.y(-(mChild.getHeight()+25)).start();
}
private void showAppBar(View child) {
hidden = false;
AppBarLayout appBarLayout = (AppBarLayout)child;
mToolbarAnimation =
GoalKicker.com – Android™ Notes for Professionals 817
appBarLayout.animate().setDuration(mContext.getResources().getInteger(android.R.integer.config_medi
umAnimTime));
mToolbarAnimation.y(mInitialY).start();
}
Here is the complete file if you need it
The second Toolbar or "Modal" toolbar:
You have to override the same methods, but in this one you have to take care of more behaviors:
show/hide the ToolBar with animations
change status bar color/background
show/hide the BottomSheet title in the ToolBar
close the bottomSheet or send it to collapsed state
The code for this one is a little extensive, so I will let the link
The FAB
This is a Custom Behavior too, but extends from FloatingActionButton.Behavior. In onDependentViewChanged you
have to look when it reach the "offSet" or point in where you want to hide it. In my case I want to hide it when it's
near to the second toolbar, so I dig into FAB parent (a CoordinatorLayout) looking for the AppBarLayout that
contains the ToolBar, then I use the ToolBar position like OffSet:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View
dependency) {
if (offset == 0)
setOffsetValue(parent);
if (dependency.getY() <=0)
return false;
if (child.getY() <= (offset + child.getHeight()) && child.getVisibility() == View.VISIBLE)
child.hide();
else if (child.getY() > offset && child.getVisibility() != View.VISIBLE)
child.show();
return false;
}
Complete Custom FAB Behavior link
The image behind the BottomSheet with parallax effect:
Like the others, it's a custom behavior, the only "complicated" thing in this one is the little algorithm that keeps the
image anchored to the BottomSheet and avoid the image collapse like default parallax effect:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
if (mYmultiplier == 0) {
initValues(child, dependency);
GoalKicker.com – Android™ Notes for Professionals 818
return true;
}
float dVerticalScroll = dependency.getY() - mPreviousY;
mPreviousY = dependency.getY();
//going up
if (dVerticalScroll <= 0 && child.getY() <= 0) {
child.setY(0);
return true;
}
//going down
if (dVerticalScroll >= 0 && dependency.getY() <= mImageHeight)
return false;
child.setY( (int)(child.getY() + (dVerticalScroll * mYmultiplier) ) );
return true;
}
The complete file for backdrop image with parallax effect
Now for the end: The Custom BottomSheet Behavior
To achieve the 3 steps, first you need to understand that default BottomSheetBehavior has 5 states:
STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN and for the Google Maps
behavior you need to add a middle state between collapsed and expanded: STATE_ANCHOR_POINT.
I tried to extend the default bottomSheetBehavior with no success, so I just copy pasted all the code and modified
what I need.
To achieve what I'm talking about follow the next steps:
1. Create a Java class and extend it from CoordinatorLayout.Behavior<V>
2. Copy paste code from default BottomSheetBehavior file to your new one.
3. Modify the method clampViewPositionVertical with the following code:
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return constrain(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset);
}
int constrain(int amount, int low, int high) {
return amount < low ? low : (amount > high ? high : amount);
}
4. Add a new state
public static final int STATE_ANCHOR_POINT = X;
5. Modify the next methods: onLayoutChild, onStopNestedScroll, BottomSheetBehavior<V> from(V view)
and setState (optional)
GoalKicker.com – Android™ Notes for Professionals 819
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
// First let the parent lay it out
if (mState != STATE_DRAGGING && mState != STATE_SETTLING) {
if (ViewCompat.getFitsSystemWindows(parent) &&
!ViewCompat.getFitsSystemWindows(child)) {
ViewCompat.setFitsSystemWindows(child, true);
}
parent.onLayoutChild(child, layoutDirection);
}
// Offset the bottom sheet
mParentHeight = parent.getHeight();
mMinOffset = Math.max(0, mParentHeight - child.getHeight());
mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset);
//if (mState == STATE_EXPANDED) {
// ViewCompat.offsetTopAndBottom(child, mMinOffset);
//} else if (mHideable && mState == STATE_HIDDEN...
if (mState == STATE_ANCHOR_POINT) {
ViewCompat.offsetTopAndBottom(child, mAnchorPoint);
} else if (mState == STATE_EXPANDED) {
ViewCompat.offsetTopAndBottom(child, mMinOffset);
} else if (mHideable && mState == STATE_HIDDEN) {
ViewCompat.offsetTopAndBottom(child, mParentHeight);
} else if (mState == STATE_COLLAPSED) {
ViewCompat.offsetTopAndBottom(child, mMaxOffset);
}
if (mViewDragHelper == null) {
mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
}
mViewRef = new WeakReference<>(child);
mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
return true;
}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
if (child.getTop() == mMinOffset) {
setStateInternal(STATE_EXPANDED);
return;
}
if (target != mNestedScrollingChildRef.get() || !mNestedScrolled) {
return;
}
int top;
int targetState;
if (mLastNestedScrollDy > 0) {
//top = mMinOffset;
//targetState = STATE_EXPANDED;
int currentTop = child.getTop();
if (currentTop > mAnchorPoint) {
top = mAnchorPoint;
targetState = STATE_ANCHOR_POINT;
}
else {
top = mMinOffset;
targetState = STATE_EXPANDED;
}
} else if (mHideable && shouldHide(child, getYVelocity())) {
top = mParentHeight;
targetState = STATE_HIDDEN;
} else if (mLastNestedScrollDy == 0) {
int currentTop = child.getTop();
GoalKicker.com – Android™ Notes for Professionals 820
if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
top = mMinOffset;
targetState = STATE_EXPANDED;
} else {
top = mMaxOffset;
targetState = STATE_COLLAPSED;
}
} else {
//top = mMaxOffset;
//targetState = STATE_COLLAPSED;
int currentTop = child.getTop();
if (currentTop > mAnchorPoint) {
top = mMaxOffset;
targetState = STATE_COLLAPSED;
}
else {
top = mAnchorPoint;
targetState = STATE_ANCHOR_POINT;
}
}
if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
setStateInternal(STATE_SETTLING);
ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
} else {
setStateInternal(targetState);
}
mNestedScrolled = false;
}
public final void setState(@State int state) {
if (state == mState) {
return;
}
if (mViewRef == null) {
// The view is not laid out yet; modify mState and let onLayoutChild handle it later
/**
* New behavior (added: state == STATE_ANCHOR_POINT ||)
*/
if (state == STATE_COLLAPSED || state == STATE_EXPANDED ||
state == STATE_ANCHOR_POINT ||
(mHideable && state == STATE_HIDDEN)) {
mState = state;
}
return;
}
V child = mViewRef.get();
if (child == null) {
return;
}
int top;
if (state == STATE_COLLAPSED) {
top = mMaxOffset;
} else if (state == STATE_ANCHOR_POINT) {
top = mAnchorPoint;
} else if (state == STATE_EXPANDED) {
top = mMinOffset;
} else if (mHideable && state == STATE_HIDDEN) {
top = mParentHeight;
} else {
throw new IllegalArgumentException("Illegal state argument: " + state);
}
setStateInternal(STATE_SETTLING);
GoalKicker.com – Android™ Notes for Professionals 821
if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));
}
}
public static <V extends View> BottomSheetBehaviorGoogleMapsLike<V> from(V view) {
ViewGroup.LayoutParams params = view.getLayoutParams();
if (!(params instanceof CoordinatorLayout.LayoutParams)) {
throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
}
CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
.getBehavior();
if (!(behavior instanceof BottomSheetBehaviorGoogleMapsLike)) {
throw new IllegalArgumentException(
"The view is not associated with BottomSheetBehaviorGoogleMapsLike");
}
return (BottomSheetBehaviorGoogleMapsLike<V>) behavior;
}
Link to the whole project where you can see all the Custom Behaviors
And here it is how it looks like:
[ ]
GoalKicker.com – Android™ Notes for Professionals 822
Section 153.3: Modal bottom sheets with BottomSheetDialog
The BottomSheetDialog is a dialog styled as a bottom sheet
Just use:
//Create a new BottomSheetDialog
BottomSheetDialog dialog = new BottomSheetDialog(context);
//Inflate the layout R.layout.my_dialog_layout
dialog.setContentView(R.layout.my_dialog_layout);
//Show the dialog
dialog.show();
In this case you don't need to attach a BottomSheet behavior.
Section 153.4: Modal bottom sheets with
BottomSheetDialogFragment
You can realize a modal bottom sheets using a BottomSheetDialogFragment.
The BottomSheetDialogFragment is a modal bottom sheet.
This is a version of DialogFragment that shows a bottom sheet using BottomSheetDialog instead of a floating
dialog.
Just define the fragment:
public class MyBottomSheetDialogFragment extends BottomSheetDialogFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.my_fragment_bottom_sheet, container);
}
}
Then use this code to show the fragment:
MyBottomSheetDialogFragment mySheetDialog = new MyBottomSheetDialogFragment();
FragmentManager fm = getSupportFragmentManager();
mySheetDialog.show(fm, "modalSheetDialog");
This Fragment will create a BottomSheetDialog.
Section 153.5: Persistent Bottom Sheets
You can achieve a Persistent Bottom Sheet attaching a BottomSheetBehavior to a child View of a
CoordinatorLayout:
<android.support.design.widget.CoordinatorLayout >
<!-- ..... -->
<LinearLayout
android:id="@+id/bottom_sheet"
android:elevation="4dp"
android:minHeight="120dp"
GoalKicker.com – Android™ Notes for Professionals 823
app:behavior_peekHeight="120dp"
...
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<!-- ..... -->
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
Then in your code you can create a reference using:
// The View with the BottomSheetBehavior
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
BottomSheetBehavior mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
You can set the state of your BottomSheetBehavior using the setState() method:
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
You can use one of these states:
STATE_COLLAPSED: this collapsed state is the default and shows just a portion of the layout along the bottom.
The height can be controlled with the app:behavior_peekHeight attribute (defaults to 0)
STATE_EXPANDED: the fully expanded state of the bottom sheet, where either the whole bottom sheet is visible
(if its height is less than the containing CoordinatorLayout) or the entire CoordinatorLayout is filled
STATE_HIDDEN: disabled by default (and enabled with the app:behavior_hideable attribute), enabling this
allows users to swipe down on the bottom sheet to completely hide the bottom sheet
If you’d like to receive callbacks of state changes, you can add a BottomSheetCallback:
mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
// React to state change
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
// React to dragging events
}
});
Section 153.6: Open BottomSheet DialogFragment in
Expanded mode by default
BottomSheet DialogFragment opens up in STATE_COLLAPSED by default. Which can be forced to open to
STATE_EXPANDEDand take up the full device screen with help of the following code template.
@NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) {
BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
GoalKicker.com – Android™ Notes for Professionals 824
BottomSheetDialog d = (BottomSheetDialog) dialog;
FrameLayout bottomSheet = (FrameLayout)
d.findViewById(android.support.design.R.id.design_bottom_sheet);
BottomSheetBehavior.from(bottomSheet).setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
// Do something with your dialog like setContentView() or whatever
return dialog;
}
Although dialog animation is slightly noticeable but does the task of opening the DialogFragment in full screen very
well.
GoalKicker.com – Android™ Notes for Professionals 825
Chapter 154: EditText
Section 154.1: Working with EditTexts
The EditText is the standard text entry widget in Android apps. If the user needs to enter text into an app, this is the
primary way for them to do that.
EditText
There are many important properties that can be set to customize the behavior of an EditText. Several of these are
listed below. Check out the official text fields guide for even more input field details.
Usage
An EditText is added to a layout with all default behaviors with the following XML:
<EditText
android:id="@+id/et_simple"
android:layout_height="wrap_content"
android:layout_width="match_parent">
</EditText>
Note that an EditText is simply a thin extension of the TextView and inherits all of the same properties.
Retrieving the Value
Getting the value of the text entered into an EditText is as follows:
EditText simpleEditText = (EditText) findViewById(R.id.et_simple);
String strValue = simpleEditText.getText().toString();
Further Entry Customization
We might want to limit the entry to a single-line of text (avoid newlines):
<EditText
android:singleLine="true"
android:lines="1"
/>
You can limit the characters that can be entered into a field using the digits attribute:
<EditText
android:inputType="number"
android:digits="01"
/>
This would restrict the digits entered to just "0" and "1". We might want to limit the total number of
characters with:
<EditText
android:maxLength="5"
/>
Using these properties we can define the expected input behavior for text fields.
GoalKicker.com – Android™ Notes for Professionals 826
Adjusting Colors
You can adjust the highlight background color of selected text within an EditText with the
android:textColorHighlight property:
<EditText
android:textColorHighlight="#7cff88"
/>
Displaying Placeholder Hints
You may want to set the hint for the EditText control to prompt a user for specific input with:
<EditText
...
android:hint="@string/my_hint">
</EditText>
Hints
Changing the bottom line color
Assuming you are using the AppCompat library, you can override the styles colorControlNormal,
colorControlActivated, and colorControlHighlight:
<style name="Theme.App.Base" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorControlNormal">#d32f2f</item>
<item name="colorControlActivated">#ff5722</item>
<item name="colorControlHighlight">#f44336</item>
</style>
If you do not see these styles applied within a DialogFragment, there is a known bug when using the LayoutInflater
passed into the onCreateView() method.
The issue has already been fixed in the AppCompat v23 library. See this guide about how to upgrade. Another
temporary workaround is to use the Activity's layout inflater instead of the one passed into the onCreateView()
method:
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_fragment, container);
}
Listening for EditText Input
Check out the basic event listeners cliffnotes for a look at how to listen for changes to an EditText and perform an
action when those changes occur.
Displaying Floating Label Feedback
Traditionally, the EditText hides the hint message (explained above) after the user starts typing. In addition, any
validation error messages had to be managed manually by the developer.
With the TextInputLayout you can setup a floating label to display hints and error messages. You can find more
details here.
GoalKicker.com – Android™ Notes for Professionals 827
Section 154.2: Customizing the InputType
Text fields can have different input types, such as number, date, password, or email address. The type determines
what kind of characters are allowed inside the field, and may prompt the virtual keyboard to optimize its layout for
frequently used characters.
By default, any text contents within an EditText control is displayed as plain text. By setting the inputType
attribute, we can facilitate input of different types of information, like phone numbers and passwords:
<EditText
...
android:inputType="phone">
</EditText>
Most common input types include:
Type Description
textUri Text that will be used as a URI
textEmailAddress Text that will be used as an e-mail address
textPersonName Text that is the name of a person
textPassword Text that is a password that should be obscured
number A numeric only field
phone For entering a phone number
date For entering a date
time For entering a time
textMultiLine Allow multiple lines of text in the field
The android:inputType also allows you to specify certain keyboard behaviors, such as whether to capitalize all new
words or use features like auto-complete and spelling suggestions.
Here are some of the common input type values that define keyboard behaviors:
Type Description
textCapSentences Normal text keyboard that capitalizes the first letter for each new sentence
textCapWords Normal text keyboard that capitalizes every word. Good for titles or person names
textAutoCorrect Normal text keyboard that corrects commonly misspelled words
You can set multiple inputType attributes if needed (separated by '|').
Example:
<EditText
android:id="@+id/postal_address"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="@string/postal_address_hint"
android:inputType="textPostalAddress|
textCapWords|
textNoSuggestions" />
You can see a list of all available input types here.
Section 154.3: Icon or button inside Custom Edit Text and its
GoalKicker.com – Android™ Notes for Professionals 828
action and click listeners
This example will help to have the Edit text with the icon at the right side.
Note: In this just I am using setCompoundDrawablesWithIntrinsicBounds, So if you want to change the
icon position you can achieve that using setCompoundDrawablesWithIntrinsicBounds in setIcon.
public class MKEditText extends AppCompatEditText {
public interface IconClickListener {
public void onClick();
}
private IconClickListener mIconClickListener;
private static final String TAG = MKEditText.class.getSimpleName();
private final int EXTRA_TOUCH_AREA = 50;
private Drawable mDrawable;
private boolean touchDown;
public MKEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MKEditText(Context context) {
super(context);
}
public MKEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void showRightIcon() {
mDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_android_black_24dp);
setIcon();
}
public void setIconClickListener(IconClickListener iconClickListener) {
mIconClickListener = iconClickListener;
}
private void setIcon() {
Drawable[] drawables = getCompoundDrawables();
setCompoundDrawablesWithIntrinsicBounds(drawables[0], drawables[1], mDrawable,
drawables[3]);
setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
setSelection(getText().length());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int right = getRight();
final int drawableSize = getCompoundPaddingRight();
final int x = (int) event.getX();
switch (event.getAction()) {
GoalKicker.com – Android™ Notes for Professionals 829
case MotionEvent.ACTION_DOWN:
if (x + EXTRA_TOUCH_AREA >= right - drawableSize && x <= right + EXTRA_TOUCH_AREA)
{
touchDown = true;
return true;
}
break;
case MotionEvent.ACTION_UP:
if (x + EXTRA_TOUCH_AREA >= right - drawableSize && x <= right + EXTRA_TOUCH_AREA
&& touchDown) {
touchDown = false;
if (mIconClickListener != null) {
mIconClickListener.onClick();
}
return true;
}
touchDown = false;
break;
}
return super.onTouchEvent(event);
}
}
If you want to change the touch area you can change the EXTRA_TOUCH_AREA values default I gave as 50.
And for Enable the button and click listener you can call from your Activity or Fragment like this,
MKEditText mkEditText = (MKEditText) findViewById(R.id.password);
mkEditText.showRightIcon();
mkEditText.setIconClickListener(new MKEditText.IconClickListener() {
@Override
public void onClick() {
// You can do action here for the icon.
}
});
Section 154.4: Hiding SoftKeyboard
Hiding Softkeyboard is a basic requirement usually when working with EditText. The softkeyboard by default can
only be closed by pressing back button and so most developers use InputMethodManager to force Android to hide
the virtual keyboard calling hideSoftInputFromWindow and passing in the token of the window containing your
focused view. The code to do the following:
public void hideSoftKeyboard()
{
InputMethodManager inputMethodManager = (InputMethodManager)
getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
The code is direct, but another major problems that arises is that the hide function needs to be called when some
event occurs. What to do when you need the Softkeyboard hidden upon pressing anywhere other than your
EditText? The following code gives a neat function that needs to be called in your onCreate() method just once.
GoalKicker.com – Android™ Notes for Professionals 830
public void setupUI(View view)
{
String s = "inside";
//Set up touch listener for non-text box views to hide keyboard.
if (!(view instanceof EditText)) {
view.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
hideSoftKeyboard();
return false;
}
});
}
//If a layout container, iterate over children and seed recursion.
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
View innerView = ((ViewGroup) view).getChildAt(i);
setupUI(innerView);
}
}
}
Section 154.5: `inputype` attribute
inputype attribute in EditText widget: (tested on Android 4.4.3 and 2.3.3)
<EditText android:id="@+id/et_test" android:inputType="?????"/>
textLongMessage= Keyboard: alphabet/default. Enter button: Send/Next. Emotion: yes. Case: lowercase.
Suggestion: yes. Add. chars: , and . and everything
textFilter= Keyboard: alphabet/default. Enter button: Send/Next. Emotion: yes. Case: lowercase. Suggestion: no.
Add. chars: , and . and everything
textCapWords= Keyboard: alphabet/default. Enter button: Send/Next. Emotion: yes. Case: Camel Case.
Suggestion: yes. Add. chars: , and . and everything
textCapSentences= Keyboard: alphabet/default. Enter button: Send/Next. Emotion: yes. Case: Sentence case.
Suggestion: yes. Add. chars: , and . and everything
time= Keyboard: numeric. Enter button: Send/Next. Emotion: no. Case: -. Suggestion: no. Add. chars: :
textMultiLine= Keyboard: alphabet/default. Enter button: nextline. Emotion: yes. Case: lowercase. Suggestion:
yes. Add. chars: , and . and everything
number= Keyboard: numeric. Enter button: Send/Next. Emotion: no. Case: -. Suggestion: no. Add. chars: nothing
textEmailAddress= Keyboard: alphabet/default. Enter button: Send/Next. Emotion: no. Case: lowercase.
Suggestion: no. Add. chars: @ and . and everything
(No type)= Keyboard: alphabet/default. Enter button: nextline. Emotion: yes. Case: lowercase. Suggestion: yes.
GoalKicker.com – Android™ Notes for Professionals 831
Add. chars: , and . and everything
textPassword= Keyboard: alphabet/default. Enter button: Send/Next. Emotion: no. Case: lowercase. Suggestion:
no. Add. chars: , and . and everything
text= Keyboard: Keyboard: alphabet/default. Enter button: Send/Next. Emotion: yes. Case: lowercase. Suggestion:
yes. Add. chars: , and . and everything
textShortMessage= Keyboard: alphabet/default. Enter button: emotion. Emotion: yes. Case: lowercase.
Suggestion: yes. Add. chars: , and . and everything
textUri= Keyboard: alphabet/default. Enter button: Send/Next. Emotion: no. Case: lowercase. Suggestion: no. Add.
chars: / and . and everything
textCapCharacters= Keyboard: alphabet/default. Enter button: Send/Next. Emotion: yes. Case: UPPERCASE.
Suggestion: yes. Add. chars: , and . and everything
phone= Keyboard: numeric. Enter button: Send/Next. Emotion: no. Case: -. Suggestion: no. Add. chars: *** # . - /
() W P N , +**
textPersonName= Keyboard: alphabet/default. Enter button: Send/Next. Emotion: yes. Case: lowercase.
Suggestion: yes. Add. chars: , and . and everything
Note: Auto-capitalization setting will change the default behavior.
Note 2: In the Numeric keyboard, ALL numbers are English 1234567890.
Note 3: Correction/Suggestion setting will change the default behavior.
GoalKicker.com – Android™ Notes for Professionals 832
Chapter 155: Speech to Text Conversion
Section 155.1: Speech to Text With Default Google Prompt
Dialog
Trigger speech to text translation
private void startListening() {
//Intent to listen to user vocal input and return result in same activity
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
//Use a language model based on free-form speech recognition.
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault());
//Message to display in dialog box
intent.putExtra(RecognizerIntent.EXTRA_PROMPT,
getString(R.string.speech_to_text_info));
try {
startActivityForResult(intent, REQ_CODE_SPEECH_INPUT);
} catch (ActivityNotFoundException a) {
Toast.makeText(getApplicationContext(),
getString(R.string.speech_not_supported),
Toast.LENGTH_SHORT).show();
}
}
Get translated results in onActivityResult
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case REQ_CODE_SPEECH_INPUT: {
if (resultCode == RESULT_OK && null != data) {
ArrayList<String> result = data
.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
txtSpeechInput.setText(result.get(0));
}
break;
}
}
}
Output
GoalKicker.com – Android™ Notes for Professionals 833
Section 155.2: Speech to Text without Dialog
The following code can be used to trigger speech-to-text translation without showing a dialog:
public void startListeningWithoutDialog() {
// Intent to listen to user vocal input and return the result to the same activity.
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
// Use a language model based on free-form speech recognition.
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault());
intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 5);
intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
appContext.getPackageName());
// Add custom listeners.
CustomRecognitionListener listener = new CustomRecognitionListener();
SpeechRecognizer sr = SpeechRecognizer.createSpeechRecognizer(appContext);
sr.setRecognitionListener(listener);
sr.startListening(intent);
}
The custom listener class CustomRecognitionListener used in the code above is implemented as follows:
class CustomRecognitionListener implements RecognitionListener {
private static final String TAG = "RecognitionListener";
public void onReadyForSpeech(Bundle params) {
Log.d(TAG, "onReadyForSpeech");
}
public void onBeginningOfSpeech() {
Log.d(TAG, "onBeginningOfSpeech");
}
public void onRmsChanged(float rmsdB) {
Log.d(TAG, "onRmsChanged");
}
public void onBufferReceived(byte[] buffer) {
Log.d(TAG, "onBufferReceived");
}
public void onEndOfSpeech() {
Log.d(TAG, "onEndofSpeech");
}
GoalKicker.com – Android™ Notes for Professionals 834
public void onError(int error) {
Log.e(TAG, "error " + error);
conversionCallaback.onErrorOccured(TranslatorUtil.getErrorText(error));
}
public void onResults(Bundle results) {
ArrayList<String> result = data
.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
txtSpeechInput.setText(result.get(0));
}
public void onPartialResults(Bundle partialResults) {
Log.d(TAG, "onPartialResults");
}
public void onEvent(int eventType, Bundle params) {
Log.d(TAG, "onEvent " + eventType);
}
}
GoalKicker.com – Android™ Notes for Professionals 835
Chapter 156: Installing apps with ADB
Section 156.1: Uninstall an app
Write the following command in your terminal to uninstall an app with a provided package name:
adb uninstall <packagename>
Section 156.2: Install all apk file in directory
Windows :
for %f in (C:\your_app_path\*.apk) do adb install "%f"
Linux :
for f in *.apk ; do adb install "$f" ; done
Section 156.3: Install an app
Write the following command in your terminal:
adb install [-rtsdg] <file>
Note that you have to pass a file that is on your computer and not on your device.
If you append -r at the end, then any existing conflicting apks will be overwritten. Otherwise, the command will quit
with an error.
-g will immediately grant all runtime permissions.
-d allows version code downgrade (only appliable on debuggable packages).
Use -s to install the application on the external SD card.
-t will allow to use test applications.
GoalKicker.com – Android™ Notes for Professionals 836
Chapter 157: Count Down Timer
Parameter Details
long millisInFuture The total duration the timer will run for, a.k.a how far in the future you want the timer
to end. In milliseconds.
long countDownInterval The interval at which you would like to receive timer updates. In milliseconds.
long millisUntilFinished A parameter provided in onTick() that tells how long the CountDownTimer has
remaining. In milliseconds
Section 157.1: Creating a simple countdown timer
CountDownTimer is useful for repeatedly performing an action in a steady interval for a set duration. In this
example, we will update a text view every second for 30 seconds telling how much time is remaining. Then when
the timer finishes, we will set the TextView to say "Done."
TextView textView = (TextView)findViewById(R.id.text_view);
CountDownTimer countDownTimer = new CountDownTimer(30000, 1000) {
public void onTick(long millisUntilFinished) {
textView.setText(String.format(Locale.getDefault(), "%d sec.", millisUntilFinished /
1000L));
}
public void onFinish() {
textView.setText("Done.");
}
}.start();
Section 157.2: A More Complex Example
In this example, we will pause/resume the CountDownTimer based off of the Activity lifecycle.
private static final long TIMER_DURATION = 60000L;
private static final long TIMER_INTERVAL = 1000L;
private CountDownTimer mCountDownTimer;
private TextView textView;
private long mTimeRemaining;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView)findViewById(R.id.text_view); // Define in xml layout.
mCountDownTimer = new CountDownTimer(TIMER_DURATION, TIMER_INTERVAL) {
@Override
public void onTick(long millisUntilFinished) {
textView.setText(String.format(Locale.getDefault(), "%d sec.", millisUntilFinished /
1000L));
mTimeRemaining = millisUntilFinished; // Saving timeRemaining in Activity for
pause/resume of CountDownTimer.
}
GoalKicker.com – Android™ Notes for Professionals 837
@Override
public void onFinish() {
textView.setText("Done.");
}
}.start();
}
@Override
protected void onResume() {
super.onResume();
if (mCountDownTimer == null) { // Timer was paused, re-create with saved time.
mCountDownTimer = new CountDownTimer(timeRemaining, INTERVAL) {
@Override
public void onTick(long millisUntilFinished) {
textView.setText(String.format(Locale.getDefault(), "%d sec.", millisUntilFinished
/ 1000L));
timeRemaining = millisUntilFinished;
}
@Override
public void onFinish() {
textView.setText("Done.");
}
}.start();
}
}
@Override
protected void onPause() {
super.onPause();
mCountDownTimer.cancel();
mCountDownTimer = null;
}
GoalKicker.com – Android™ Notes for Professionals 838
Chapter 158: Barcode and QR code
reading
Section 158.1: Using QRCodeReaderView (based on Zxing)
QRCodeReaderView implements an Android view which show camera and notify when there's a QR code inside the
preview.
It uses the zxing open-source, multi-format 1D/2D barcode image processing library.
Adding the library to your project
Add QRCodeReaderView dependency to your build.gradle
dependencies{
compile 'com.dlazaro66.qrcodereaderview:qrcodereaderview:2.0.0'
}
First use
Add to your layout a QRCodeReaderView
<com.dlazaro66.qrcodereaderview.QRCodeReaderView
android:id="@+id/qrdecoderview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Create an Activity which implements onQRCodeReadListener, and use it as a listener of the
QrCodeReaderView.
Make sure you have camera permissions in order to use the library.
(https://developer.android.com/training/permissions/requesting.html)
Then in your Activity, you can use it as follows:
public class DecoderActivity extends Activity implements OnQRCodeReadListener {
private TextView resultTextView;
private QRCodeReaderView qrCodeReaderView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_decoder);
qrCodeReaderView = (QRCodeReaderView) findViewById(R.id.qrdecoderview);
qrCodeReaderView.setOnQRCodeReadListener(this);
// Use this function to enable/disable decoding
qrCodeReaderView.setQRDecodingEnabled(true);
// Use this function to change the autofocus interval (default is 5 secs)
qrCodeReaderView.setAutofocusInterval(2000L);
// Use this function to enable/disable Torch
qrCodeReaderView.setTorchEnabled(true);
// Use this function to set front camera preview
qrCodeReaderView.setFrontCamera();
GoalKicker.com – Android™ Notes for Professionals 839
// Use this function to set back camera preview
qrCodeReaderView.setBackCamera();
}
// Called when a QR is decoded
// "text" : the text encoded in QR
// "points" : points where QR control points are placed in View
@Override
public void onQRCodeRead(String text, PointF[] points) {
resultTextView.setText(text);
}
@Override
protected void onResume() {
super.onResume();
qrCodeReaderView.startCamera();
}
@Override
protected void onPause() {
super.onPause();
qrCodeReaderView.stopCamera();
}
}
GoalKicker.com – Android™ Notes for Professionals 840
Chapter 159: Android PayPal Gateway
Integration
Section 159.1: Setup PayPal in your android code
1) First go through Paypal Developer web site and create an application.
2) Now open your manifest file and give the below permissions
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
3) And some required Activity and Services
<service
android:name="com.paypal.android.sdk.payments.PayPalService"
android:exported="false" />
<activity android:name="com.paypal.android.sdk.payments.PaymentActivity" />
<activity android:name="com.paypal.android.sdk.payments.LoginActivity" />
<activity android:name="com.paypal.android.sdk.payments.PaymentMethodActivity" />
<activity android:name="com.paypal.android.sdk.payments.PaymentConfirmActivity" />
<activity android:name="com.paypal.android.sdk.payments.PayPalFuturePaymentActivity" />
<activity android:name="com.paypal.android.sdk.payments.FuturePaymentConsentActivity" />
<activity android:name="com.paypal.android.sdk.payments.FuturePaymentInfoActivity" />
<activity
android:name="io.card.payment.CardIOActivity"
android:configChanges="keyboardHidden|orientation" />
<activity android:name="io.card.payment.DataEntryActivity" />
4) Open your Activity class and set Configuration for your app
//set the environment for production/sandbox/no netowrk
private static final String CONFIG_ENVIRONMENT = PayPalConfiguration.ENVIRONMENT_PRODUCTION;
5) Now set client id from the Paypal developer account
private static final String CONFIG_CLIENT_ID = "PUT YOUR CLIENT ID";
6) Inside onCreate method call the Paypal service
Intent intent = new Intent(this, PayPalService.class);
intent.putExtra(PayPalService.EXTRA_PAYPAL_CONFIGURATION, config);
startService(intent);
7) Now you are ready to make a payment just on button press call the Payment Activity
PayPalPayment thingToBuy = new PayPalPayment(new BigDecimal(1),"USD", "androidhub4you.com",
PayPalPayment.PAYMENT_INTENT_SALE);
Intent intent = new Intent(MainActivity.this, PaymentActivity.class);
intent.putExtra(PaymentActivity.EXTRA_PAYMENT, thingToBuy);
startActivityForResult(intent, REQUEST_PAYPAL_PAYMENT);
8) And finally from the onActivityResult get the payment response
GoalKicker.com – Android™ Notes for Professionals 841
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_PAYPAL_PAYMENT) {
if (resultCode == Activity.RESULT_OK) {
PaymentConfirmation confirm = data
.getParcelableExtra(PaymentActivity.EXTRA_RESULT_CONFIRMATION);
if (confirm != null) {
try {
System.out.println("Responseeee"+confirm);
Log.i("paymentExample", confirm.toJSONObject().toString());
JSONObject jsonObj=new
JSONObject(confirm.toJSONObject().toString());
String paymentId=jsonObj.getJSONObject("response").getString("id");
System.out.println("payment id:-=="+paymentId);
Toast.makeText(getApplicationContext(), paymentId,
Toast.LENGTH_LONG).show();
} catch (JSONException e) {
Log.e("paymentExample", "an extremely unlikely failure occurred: ",
e);
}
}
} else if (resultCode == Activity.RESULT_CANCELED) {
Log.i("paymentExample", "The user canceled.");
} else if (resultCode == PaymentActivity.RESULT_EXTRAS_INVALID) {
Log.i("paymentExample", "An invalid Payment was submitted. Please see the
docs.");
}
}
}
GoalKicker.com – Android™ Notes for Professionals 842
Chapter 160: Drawables
Section 160.1: Custom Drawable
Extend your class with Drawable and override these methods
public class IconDrawable extends Drawable {
/**
* Paint for drawing the shape
*/
private Paint paint;
/**
* Icon drawable to be drawn to the center of the shape
*/
private Drawable icon;
/**
* Desired width and height of icon
*/
private int desiredIconHeight, desiredIconWidth;
/**
* Public constructor for the Icon drawable
*
* @param icon pass the drawable of the icon to be drawn at the center
* @param backgroundColor background color of the shape
*/
public IconDrawable(Drawable icon, int backgroundColor) {
this.icon = icon;
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(backgroundColor);
desiredIconWidth = 50;
desiredIconHeight = 50;
}
@Override
public void draw(Canvas canvas) {
//if we are setting this drawable to a 80dpX80dp imageview
//getBounds will return that measurements,we can draw according to that width.
Rect bounds = getBounds();
//drawing the circle with center as origin and center distance as radius
canvas.drawCircle(bounds.centerX(), bounds.centerY(), bounds.centerX(), paint);
//set the icon drawable's bounds to the center of the shape
icon.setBounds(bounds.centerX() - (desiredIconWidth / 2), bounds.centerY() -
(desiredIconHeight / 2), (bounds.centerX() - (desiredIconWidth / 2)) + desiredIconWidth,
(bounds.centerY() - (desiredIconHeight / 2)) + desiredIconHeight);
//draw the icon to the bounds
icon.draw(canvas);
}
@Override
public void setAlpha(int alpha) {
//sets alpha to your whole shape
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
//sets color filter to your whole shape
paint.setColorFilter(colorFilter);
GoalKicker.com – Android™ Notes for Professionals 843
}
@Override
public int getOpacity() {
//give the desired opacity of the shape
return PixelFormat.TRANSLUCENT;
}
}
Declare a ImageView in your layout
<ImageView
android:layout_width="80dp"
android:id="@+id/imageView"
android:layout_height="80dp" />
Set your custom drawable to the ImageView
IconDrawable iconDrawable=new
IconDrawable(ContextCompat.getDrawable(this,android.R.drawable.ic_media_play),ContextCompat.getColo
r(this,R.color.pink_300));
imageView.setImageDrawable(iconDrawable);
Screenshot
Section 160.2: Tint a drawable
A drawable can be tinted a certain color. This is useful for supporting different themes within your application, and
reducing the number of drawable resource files.
Using framework APIs on SDK 21+:
Drawable d = context.getDrawable(R.drawable.ic_launcher);
d.setTint(Color.WHITE);
Using android.support.v4 library on SDK 4+:
//Load the untinted resource
final Drawable drawableRes = ContextCompat.getDrawable(context, R.drawable.ic_launcher);
//Wrap it with the compatibility library so it can be altered
Drawable tintedDrawable = DrawableCompat.wrap(drawableRes);
//Apply a coloured tint
DrawableCompat.setTint(tintedDrawable, Color.WHITE);
//At this point you may use the tintedDrawable just as you usually would
//(and drawableRes can be discarded)
//NOTE: If your original drawableRes was in use somewhere (i.e. it was the result of
//a call to a `getBackground()` method then at this point you still need to replace
//the background. setTint does *not* alter the instance that drawableRes points to,
//but instead creates a new drawable instance
Please not that int color is not referring to a color Resource, however you are not limited to those colours defined
GoalKicker.com – Android™ Notes for Professionals 844
in the 'Color' class. When you have a colour defined in your XML which you want to use you must just first get it's
value.
You can replace usages of Color.WHITE using the methods below
When targetting older API's:
getResources().getColor(R.color.your_color);
Or on newer targets:
ContextCompat.getColor(context, R.color.your_color);
Section 160.3: Circular View
For a circular View (in this case TextView) create a drawble round_view.xml in drawble folder:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#FAA23C" />
<stroke android:color="#FFF" android:width="2dp" />
</shape>
Assign the drawable to the View:
<TextView
android:id="@+id/game_score"
android:layout_width="60dp"
android:layout_height="60dp"
android:background="@drawable/round_score"
android:padding="6dp"
android:text="100"
android:textColor="#fff"
android:textSize="20sp"
android:textStyle="bold"
android:gravity="center" />
Now it should look like the orange circle:
Section 160.4: Make View with rounded corners
Create drawable file named with custom_rectangle.xml in drawable folder:
GoalKicker.com – Android™ Notes for Professionals 845
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<solid android:color="@android:color/white" />
<corners android:radius="10dip" />
<stroke
android:width="1dp"
android:color="@android:color/white" />
</shape>
Now apply rectangle background on View:
mView.setBackGround(R.drawlable.custom_rectangle);
Reference screenshot:
GoalKicker.com – Android™ Notes for Professionals 846
Chapter 161: TransitionDrawable
Section 161.1: Animate views background color (switch-color)
with TransitionDrawable
public void setCardColorTran(View view) {
ColorDrawable[] color = {new ColorDrawable(Color.BLUE), new ColorDrawable(Color.RED)};
TransitionDrawable trans = new TransitionDrawable(color);
if(Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
view.setBackgroundDrawable(trans);
}else {
view.setBackground(trans);
}
trans.startTransition(5000);
}
Section 161.2: Add transition or Cross-fade between two
images
Step 1: Create a transition drawable in XML
Save this file transition.xml in res/drawable folder of your project.
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/image1"/>
<item android:drawable="@drawable/image2"/>
</transition>
The image1 and image2 are the two images that we want to transition and they should be put in your res/drawable
folder too.
Step 2: Add code for ImageView in your XML layout to display the above drawable.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/image1"/>
</LinearLayout>
Step 3: Access the XML transition drawable in onCreate() method of your Activity and start transition in
onClick() event.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = (ImageView) findViewById(R.id.image_view);
transitionDrawable = (TransitionDrawable)
GoalKicker.com – Android™ Notes for Professionals 847
ContextCompat.getDrawable(this, R.drawable.transition);
birdImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View view) {
birdImageView.setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(1000);
}
});
}
GoalKicker.com – Android™ Notes for Professionals 848
Chapter 162: Vector Drawables
Parameter Details
<vector> Used to define a vector drawable
<group>
Defines a group of paths or subgroups, plus transformation information. The transformations are
defined in the same coordinates as the viewport. And the transformations are applied in the order of
scale, rotate then translate.
<path> Defines paths to be drawn.
<clip-path> Defines path to be the current clip. Note that the clip path only apply to the current group and its
children.
As the name implies, vector drawables are based on vector graphics. Vector graphics are a way of describing
graphical elements using geometric shapes. This lets you create a drawable based on an XML vector graphic. Now
there is no need to design different size image for mdpi, hdpi, xhdpi and etc. With Vector Drawable you need to
create image only once as an xml file and you can scale it for all dpi and for different devices. This also not save
space but also simplifies maintenance.
Section 162.1: Importing SVG file as VectorDrawable
You can import an SVG file as a VectorDrawable in Android Studio, follow these steps :
"Right-click" on the res folder and select new > Vector Asset.
GoalKicker.com – Android™ Notes for Professionals 849
Select the Local File option and browse to your .svg file. Change the options to your liking and hit next. Done.
GoalKicker.com – Android™ Notes for Professionals 850
Section 162.2: VectorDrawable Usage Example
Here’s an example vector asset which we’re actually using in AppCompat:
res/drawable/ic_search.xml
<vector xmlns:android="..."
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:pathData="..."
android:fillColor="@android:color/white"/>
</vector>
Using this drawable, an example ImageView declaration would be:
<ImageView
GoalKicker.com – Android™ Notes for Professionals 851
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_search"/>
You can also set it at run-time:
ImageView iv = (ImageView) findViewById(...);
iv.setImageResource(R.drawable.ic_search);
The same attribute and calls work for ImageButton too.
Section 162.3: VectorDrawable xml example
Here is a simple VectorDrawable in this vectordrawable.xml file.
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="64dp"
android:width="64dp"
android:viewportHeight="600"
android:viewportWidth="600" >
<group
android:name="rotationGroup"
android:pivotX="300.0"
android:pivotY="300.0"
android:rotation="45.0" >
<path
android:name="v"
android:fillColor="#000000"
android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
</group>
</vector>
GoalKicker.com – Android™ Notes for Professionals 852
Chapter 163: VectorDrawable and
AnimatedVectorDrawable
Section 163.1: Basic VectorDrawable
A VectorDrawable should consist of at least one <path> tag defining a shape
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M0,24 l12,-24 l12,24 z"/>
</vector>
This would produce a black triangle:
Section 163.2: <group> tags
A <group> tag allows the scaling, rotation, and position of one or more elements of a VectorDrawable to be
adjusted:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M0,0 h4 v4 h-4 z"
android:fillColor="#FF000000"/>
<group
android:name="middle square group"
android:translateX="10"
android:translateY="10"
android:rotation="45">
<path
GoalKicker.com – Android™ Notes for Professionals 853
android:pathData="M0,0 h4 v4 h-4 z"
android:fillColor="#FF000000"/>
</group>
<group
android:name="last square group"
android:translateX="18"
android:translateY="18"
android:scaleX="1.5">
<path
android:pathData="M0,0 h4 v4 h-4 z"
android:fillColor="#FF000000"/>
</group>
</vector>
The example code above contains three identical <path> tags, all describing black squares. The first square is
unadjusted. The second square is wrapped in a <group> tag which moves it and rotates it by 45°. The third square is
wrapped in a <group> tag which moves it and stretches it horizontally by 50%. The result is as follows:
A <group> tag can contain multiple <path> and <clip-path> tags. It can even contain another <group>.
Section 163.3: Basic AnimatedVectorDrawable
An AnimatedVectorDrawable requires at least 3 components:
A VectorDrawable which will be manipulated
An objectAnimator which defines what property to change and how
The AnimatedVectorDrawable itself which connects the objectAnimator to the VectorDrawable to create the
animation
The following creates a triangle that transitions its color from black to red.
The VectorDrawable, filename: triangle_vector_drawable.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
GoalKicker.com – Android™ Notes for Professionals 854
android:name="triangle"
android:fillColor="@android:color/black"
android:pathData="M0,24 l12,-24 l12,24 z"/>
</vector>
The objectAnimator, filename: color_change_animator.xml
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="fillColor"
android:duration="2000"
android:repeatCount="infinite"
android:valueFrom="@android:color/black"
android:valueTo="@android:color/holo_red_light"/>
The AnimatedVectorDrawable, filename: triangle_animated_vector.xml
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/triangle_vector_drawable">
<target
android:animation="@animator/color_change_animator"
android:name="triangle"/>
</animated-vector>
Note that the <target> specifies android:name="triangle" which matches the <path> in the VectorDrawable. A
VectorDrawable may contain multiple elements and the android:name property is used to define which element is
being targeted.
Result:
Section 163.4: Using Strokes
Using SVG stroke makes it easier to create a Vector drawable with unified stroke length, as per Material Design
guidelines:
Consistent stroke weights are key to unifying the overall system icon family. Maintain a 2dp width for all
GoalKicker.com – Android™ Notes for Professionals 855
stroke instances, including curves, angles, and both interior and exterior strokes.
So, for example, this is how you would create a "plus" sign using strokes:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:strokeColor="#F000"
android:strokeWidth="2"
android:pathData="M12,0 V24 M0,12 H24" />
</vector>
strokeColor defines the color of the stroke.
strokeWidth defines the width (in dp) of the stroke (2dp in this case, as suggested by the guidelines).
pathData is where we describe our SVG image:
M12,0 moves the "cursor" to the position 12,0
V24 creates a vertical line to the position 12, 24
etc., see SVG documentation and this useful "SVG Path" tutorial from w3schools to learn more about the specific
path commands.
As a result, we got this no-frills plus sign:
This is especially useful for creating an AnimatedVectorDrawable, since you are now operating with a single stroke
with an unified length, instead of an otherwise complicated path.
GoalKicker.com – Android™ Notes for Professionals 856
Section 163.5: Using <clip-path>
A <clip-path> defines a shape which acts as a window, only allowing parts of a <path> to show if they are within
the <clip-path> shape and cutting off the rest.
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<clip-path
android:name="square clip path"
android:pathData="M6,6 h12 v12 h-12 z"/>
<path
android:name="triangle"
android:fillColor="#FF000000"
android:pathData="M0,24 l12,-24 l12,24 z"/>
</vector>
In this case the <path> produces a black triangle, but the <clip-path> defines a smaller square shape, only allowing
part of the triangle to show through:
Section 163.6: Vector compatibility through AppCompat
A few pre-requisites in the build.gradle for vectors to work all the way down to API 7 for VectorDrawables and API
13 for AnimatedVectorDrawables (with some caveats currently):
//Build Tools has to be 24+
buildToolsVersion '24.0.0'
defaultConfig {
vectorDrawables.useSupportLibrary = true
generatedDensities = []
aaptOptions {
additionalParameters "--no-version-vectors"
}
}
dependencies {
GoalKicker.com – Android™ Notes for Professionals 857
compile 'com.android.support:appcompat-v7:24.1.1'
}
In your layout.xml:
<ImageView
android:id="@+id/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
appCompat:src="@drawable/vector_drawable"
android:contentDescription="@null" />
GoalKicker.com – Android™ Notes for Professionals 858
Chapter 164: Port Mapping using Cling
library in Android
Section 164.1: Mapping a NAT port
String myIp = getIpAddress();
int port = 55555;
//creates a port mapping configuration with the external/internal port, an internal host IP, the
protocol and an optional description
PortMapping[] desiredMapping = new PortMapping[2];
desiredMapping[0] = new PortMapping(port,myIp, PortMapping.Protocol.TCP);
desiredMapping[1] = new PortMapping(port,myIp, PortMapping.Protocol.UDP);
//starting the UPnP service
UpnpService upnpService = new UpnpServiceImpl(new AndroidUpnpServiceConfiguration());
RegistryListener registryListener = new PortMappingListener(desiredMapping);
upnpService.getRegistry().addListener(registryListener);
upnpService.getControlPoint().search();
//method for getting local ip
private String getIpAddress() {
String ip = "";
try {
Enumeration<NetworkInterface> enumNetworkInterfaces = NetworkInterface
.getNetworkInterfaces();
while (enumNetworkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = enumNetworkInterfaces
.nextElement();
Enumeration<InetAddress> enumInetAddress = networkInterface
.getInetAddresses();
while (enumInetAddress.hasMoreElements()) {
InetAddress inetAddress = enumInetAddress.nextElement();
if (inetAddress.isSiteLocalAddress()) {
ip +=inetAddress.getHostAddress();
}
}
}
} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
ip += "Something Wrong! " + e.toString() + "\n";
}
return ip;
}
Section 164.2: Adding Cling Support to your Android Project
build.gradle
repositories {
maven { url 'http://4thline.org/m2' }
}
dependencies {
GoalKicker.com – Android™ Notes for Professionals 859
// Cling
compile 'org.fourthline.cling:cling-support:2.1.0'
//Other dependencies required by Cling
compile 'org.eclipse.jetty:jetty-server:8.1.18.v20150929'
compile 'org.eclipse.jetty:jetty-servlet:8.1.18.v20150929'
compile 'org.eclipse.jetty:jetty-client:8.1.18.v20150929'
compile 'org.slf4j:slf4j-jdk14:1.7.14'
}
GoalKicker.com – Android™ Notes for Professionals 860
Chapter 165: Creating Overlay (always-ontop)
Windows
Section 165.1: Popup overlay
In order to put your view on top of every application, you have to assign your view to the corresponding window
manager. For that you need the system alert permission, which can be requested by adding the following line to
your manifest file:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
Note: If your application gets destroyed, your view will be removed from the window manager. Therefore, it is better
to create the view and assign it to the window manager by a foreground service.
Assigning a view to the WindowManager
You can retrieve a window manager instance as follows:
WindowManager mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
In order to define the position of your view, you have to create some layout parameters as follows:
WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
PixelFormat.TRANSLUCENT);
mLayoutParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL;
Now, you can assign your view together with the created layout parameters to the window manager instance as
follows:
mWindowManager.addView(yourView, mLayoutParams);
Voila! Your view has been successfully placed on top of all other applications.
Note: You view will not be put on top of the keyguard.
Section 165.2: Granting SYSTEM_ALERT_WINDOW Permission
on android 6.0 and above
From android 6.0 this permission needs to grant dynamically,
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
Throwing below permission denied error on 6.0,
Caused by: android.view.WindowManager$BadTokenException: Unable to add window
android.view.ViewRootImpl$W@86fb55b -- permission denied for this window type
Solution:
GoalKicker.com – Android™ Notes for Professionals 861
Requesting Overlay permission as below,
if(!Settings.canDrawOverlays(this)){
// ask for setting
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION);
}
Check for the result,
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_OVERLAY_PERMISSION) {
if (Settings.canDrawOverlays(this)) {
// permission granted...
}else{
// permission not granted...
}
}
}
GoalKicker.com – Android™ Notes for Professionals 862
Chapter 166: ExoPlayer
Section 166.1: Add ExoPlayer to the project
Via jCenter
including the following in your project's build.gradle file:
compile 'com.google.android.exoplayer:exoplayer:rX.X.X'
where rX.X.X is the your preferred version. For the latest version, see the project's Releases. For more details, see
the project on Bintray.
Section 166.2: Using ExoPlayer
Instantiate your ExoPlayer:
exoPlayer = ExoPlayer.Factory.newInstance(RENDERER_COUNT, minBufferMs, minRebufferMs);
To play audio only you can use these values:
RENDERER_COUNT = 1 //since you want to render simple audio
minBufferMs = 1000
minRebufferMs = 5000
Both buffer values can be tweaked according to your requirements.
Now you have to create a DataSource. When you want to stream mp3 you can use the DefaultUriDataSource. You
have to pass the Context and a UserAgent. To keep it simple play a local file and pass null as userAgent:
DataSource dataSource = new DefaultUriDataSource(context, null);
Then create the sampleSource:
ExtractorSampleSource sampleSource = new ExtractorSampleSource(
uri, dataSource, new Mp3Extractor(), RENDERER_COUNT, requestedBufferSize);
uri points to your file, as an Extractor you can use a simple default Mp3Extractor if you want to play mp3.
requestedBufferSize can be tweaked again according to your requirements. Use 5000 for example.
Now you can create your audio track renderer using the sample source as follows:
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
Finally call prepare on your exoPlayer instance:
exoPlayer.prepare(audioRenderer);
To start playback call:
exoPlayer.setPlayWhenReady(true);
GoalKicker.com – Android™ Notes for Professionals 863
Section 166.3: Main steps to play video & audio using the
standard TrackRenderer implementations
// 1. Instantiate the player.
player = ExoPlayer.Factory.newInstance(RENDERER_COUNT);
// 2. Construct renderers.
MediaCodecVideoTrackRenderer videoRenderer = ...
MediaCodecAudioTrackRenderer audioRenderer = ...
// 3. Inject the renderers through prepare.
player.prepare(videoRenderer, audioRenderer);
// 4. Pass the surface to the video renderer.
player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
// 5. Start playback.
player.setPlayWhenReady(true);
...
player.release(); // Don’t forget to release when done!
GoalKicker.com – Android™ Notes for Professionals 864
Chapter 167: XMPP register login and chat
simple example
Section 167.1: XMPP register login and chat basic example
Install openfire or any chat server in your system or on server. For more details click here.
Create android project and add these libraries in gradle:
compile 'org.igniterealtime.smack:smack-android:4.2.0'
compile 'org.igniterealtime.smack:smack-tcp:4.2.0'
compile 'org.igniterealtime.smack:smack-im:4.2.0'
compile 'org.igniterealtime.smack:smack-android-extensions:4.2.0'
Next create one xmpp class from xmpp connection purpose:
public class XMPP {
public static final int PORT = 5222;
private static XMPP instance;
private XMPPTCPConnection connection;
private static String TAG = "XMPP-EXAMPLE";
public static final String ACTION_LOGGED_IN = "liveapp.loggedin";
private String HOST = "192.168.0.10";
private XMPPTCPConnectionConfiguration buildConfiguration() throws XmppStringprepException {
XMPPTCPConnectionConfiguration.Builder builder =
XMPPTCPConnectionConfiguration.builder();
builder.setHost(HOST);
builder.setPort(PORT);
builder.setCompressionEnabled(false);
builder.setDebuggerEnabled(true);
builder.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled);
builder.setSendPresence(true);
if (Build.VERSION.SDK_INT >= 14) {
builder.setKeystoreType("AndroidCAStore");
// config.setTruststorePassword(null);
builder.setKeystorePath(null);
} else {
builder.setKeystoreType("BKS");
String str = System.getProperty("javax.net.ssl.trustStore");
if (str == null) {
str = System.getProperty("java.home") + File.separator + "etc" + File.separator +
"security"
+ File.separator + "cacerts.bks";
}
builder.setKeystorePath(str);
}
DomainBareJid serviceName = JidCreate.domainBareFrom(HOST);
builder.setServiceName(serviceName);
return builder.build();
}
GoalKicker.com – Android™ Notes for Professionals 865
private XMPPTCPConnection getConnection() throws XMPPException, SmackException, IOException,
InterruptedException {
Log.logDebug(TAG, "Getting XMPP Connect");
if (isConnected()) {
Log.logDebug(TAG, "Returning already existing connection");
return this.connection;
}
long l = System.currentTimeMillis();
try {
if(this.connection != null){
Log.logDebug(TAG, "Connection found, trying to connect");
this.connection.connect();
}else{
Log.logDebug(TAG, "No Connection found, trying to create a new connection");
XMPPTCPConnectionConfiguration config = buildConfiguration();
SmackConfiguration.DEBUG = true;
this.connection = new XMPPTCPConnection(config);
this.connection.connect();
}
} catch (Exception e) {
Log.logError(TAG,"some issue with getting connection :" + e.getMessage());
}
Log.logDebug(TAG, "Connection Properties: " + connection.getHost() + " " +
connection.getServiceName());
Log.logDebug(TAG, "Time taken in first time connect: " + (System.currentTimeMillis() - l));
return this.connection;
}
public static XMPP getInstance() {
if (instance == null) {
synchronized (XMPP.class) {
if (instance == null) {
instance = new XMPP();
}
}
}
return instance;
}
public void close() {
Log.logInfo(TAG, "Inside XMPP close method");
if (this.connection != null) {
this.connection.disconnect();
}
}
private XMPPTCPConnection connectAndLogin(Context context) {
Log.logDebug(TAG, "Inside connect and Login");
if (!isConnected()) {
Log.logDebug(TAG, "Connection not connected, trying to login and connect");
try {
// Save username and password then use here
String username = AppSettings.getUser(context);
String password = AppSettings.getPassword(context);
this.connection = getConnection();
Log.logDebug(TAG, "XMPP username :" + username);
Log.logDebug(TAG, "XMPP password :" + password);
this.connection.login(username, password);
Log.logDebug(TAG, "Connect and Login method, Login successful");
GoalKicker.com – Android™ Notes for Professionals 866
context.sendBroadcast(new Intent(ACTION_LOGGED_IN));
} catch (XMPPException localXMPPException) {
Log.logError(TAG, "Error in Connect and Login Method");
localXMPPException.printStackTrace();
} catch (SmackException e) {
Log.logError(TAG, "Error in Connect and Login Method");
e.printStackTrace();
} catch (IOException e) {
Log.logError(TAG, "Error in Connect and Login Method");
e.printStackTrace();
} catch (InterruptedException e) {
Log.logError(TAG, "Error in Connect and Login Method");
e.printStackTrace();
} catch (IllegalArgumentException e) {
Log.logError(TAG, "Error in Connect and Login Method");
e.printStackTrace();
} catch (Exception e) {
Log.logError(TAG, "Error in Connect and Login Method");
e.printStackTrace();
}
}
Log.logInfo(TAG, "Inside getConnection - Returning connection");
return this.connection;
}
public boolean isConnected() {
return (this.connection != null) && (this.connection.isConnected());
}
public EntityFullJid getUser() {
if (isConnected()) {
return connection.getUser();
} else {
return null;
}
}
public void login(String user, String pass, String username)
throws XMPPException, SmackException, IOException, InterruptedException,
PurplKiteXMPPConnectException {
Log.logInfo(TAG, "inside XMPP getlogin Method");
long l = System.currentTimeMillis();
XMPPTCPConnection connect = getConnection();
if (connect.isAuthenticated()) {
Log.logInfo(TAG, "User already logged in");
return;
}
Log.logInfo(TAG, "Time taken to connect: " + (System.currentTimeMillis() - l));
l = System.currentTimeMillis();
try{
connect.login(user, pass);
}catch (Exception e){
Log.logError(TAG, "Issue in login, check the stacktrace");
e.printStackTrace();
}
Log.logInfo(TAG, "Time taken to login: " + (System.currentTimeMillis() - l));
Log.logInfo(TAG, "login step passed");
GoalKicker.com – Android™ Notes for Professionals 867
PingManager pingManager = PingManager.getInstanceFor(connect);
pingManager.setPingInterval(5000);
}
public void register(String user, String pass) throws XMPPException,
SmackException.NoResponseException, SmackException.NotConnectedException {
Log.logInfo(TAG, "inside XMPP register method, " + user + " : " + pass);
long l = System.currentTimeMillis();
try {
AccountManager accountManager = AccountManager.getInstance(getConnection());
accountManager.sensitiveOperationOverInsecureConnection(true);
accountManager.createAccount(Localpart.from(user), pass);
} catch (SmackException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (PurplKiteXMPPConnectException e) {
e.printStackTrace();
}
Log.logInfo(TAG, "Time taken to register: " + (System.currentTimeMillis() - l));
}
public void addStanzaListener(Context context, StanzaListener stanzaListener){
XMPPTCPConnection connection = connectAndLogin(context);
connection.addAsyncStanzaListener(stanzaListener, null);
}
public void removeStanzaListener(Context context, StanzaListener stanzaListener){
XMPPTCPConnection connection = connectAndLogin(context);
connection.removeAsyncStanzaListener(stanzaListener);
}
public void addChatListener(Context context, ChatManagerListener chatManagerListener){
ChatManager.getInstanceFor(connectAndLogin(context))
.addChatListener(chatManagerListener);
}
public void removeChatListener(Context context, ChatManagerListener chatManagerListener){
ChatManager.getInstanceFor(connectAndLogin(context)).removeChatListener(chatManagerListener);
}
public void getSrvDeliveryManager(Context context){
ServiceDiscoveryManager sdm = ServiceDiscoveryManager
.getInstanceFor(XMPP.getInstance().connectAndLogin(
context));
//sdm.addFeature("http://jabber.org/protocol/disco#info");
//sdm.addFeature("jabber:iq:privacy");
sdm.addFeature("jabber.org/protocol/si");
sdm.addFeature("http://jabber.org/protocol/si");
sdm.addFeature("http://jabber.org/protocol/disco#info");
sdm.addFeature("jabber:iq:privacy");
}
public String getUserLocalPart(Context context){
return connectAndLogin(context).getUser().getLocalpart().toString();
}
GoalKicker.com – Android™ Notes for Professionals 868
public EntityFullJid getUser(Context context){
return connectAndLogin(context).getUser();
}
public Chat getThreadChat(Context context, String party1, String party2){
Chat chat = ChatManager.getInstanceFor(
XMPP.getInstance().connectAndLogin(context))
.getThreadChat(party1 + "-" + party2);
return chat;
}
public Chat createChat(Context context, EntityJid jid, String party1, String party2,
ChatMessageListener messageListener){
Chat chat = ChatManager.getInstanceFor(
XMPP.getInstance().connectAndLogin(context))
.createChat(jid, party1 + "-" + party2,
messageListener);
return chat;
}
public void sendPacket(Context context, Stanza packet){
try {
connectAndLogin(context).sendStanza(packet);
} catch (SmackException.NotConnectedException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Finally, add this activiy:
private UserLoginTask mAuthTask = null;
private ChatManagerListener chatListener;
private Chat chat;
private Jid opt_jid;
private ChatMessageListener messageListener;
private StanzaListener packetListener;
private boolean register(final String paramString1,final String paramString2) {
try {
XMPP.getInstance().register(paramString1, paramString2);
return true;
} catch (XMPPException localXMPPException) {
localXMPPException.printStackTrace();
} catch (SmackException.NoResponseException e) {
e.printStackTrace();
} catch (SmackException.NotConnectedException e) {
e.printStackTrace();
}
return false;
}
private boolean login(final String user,final String pass,final String username) {
try {
XMPP.getInstance().login(user, pass, username);
GoalKicker.com – Android™ Notes for Professionals 869
sendBroadcast(new Intent("liveapp.loggedin"));
return true;
} catch (Exception e) {
e.printStackTrace();
try {
XMPP.getInstance()
.login(user, pass, username);
sendBroadcast(new Intent("liveapp.loggedin"));
return true;
} catch (XMPPException e1) {
e1.printStackTrace();
} catch (SmackException e1) {
e1.printStackTrace();
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}catch (Exception e1){
e1.printStackTrace();
}
}
return false;
}
public class UserLoginTask extends AsyncTask<Void, Void, Boolean> {
public UserLoginTask() {
}
protected Boolean doInBackground(Void... paramVarArgs) {
String mEmail = "abc";
String mUsername = "abc";
String mPassword = "welcome";
if (register(mEmail, mPassword)) {
try {
XMPP.getInstance().close();
} catch (Exception e) {
e.printStackTrace();
}
}
return login(mEmail, mPassword, mUsername);
}
protected void onCancelled() {
mAuthTask = null;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
protected void onPostExecute(Boolean success) {
mAuthTask = null;
try {
GoalKicker.com – Android™ Notes for Professionals 870
if (success) {
messageListener = new ChatMessageListener() {
@Override
public void processMessage(Chat chat, Message message) {
// here you will get only connected user by you
}
};
packetListener = new StanzaListener() {
@Override
public void processPacket(Stanza packet) throws
SmackException.NotConnectedException, InterruptedException {
if (packet instanceof Message) {
final Message message = (Message) packet;
// here you will get all messages send by anybody
}
}
};
chatListener = new ChatManagerListener() {
@Override
public void chatCreated(Chat chatCreated, boolean local) {
onChatCreated(chatCreated);
}
};
try {
String opt_jidStr = "abc";
try {
opt_jid = JidCreate.bareFrom(Localpart.from(opt_jidStr), Domainpart.from(HOST));
} catch (XmppStringprepException e) {
e.printStackTrace();
}
String addr1 = XMPP.getInstance().getUserLocalPart(getActivity());
String addr2 = opt_jid.toString();
if (addr1.compareTo(addr2) > 0) {
String addr3 = addr2;
addr2 = addr1;
addr1 = addr3;
}
chat = XMPP.getInstance().getThreadChat(getActivity(), addr1, addr2);
if (chat == null) {
chat = XMPP.getInstance().createChat(getActivity(), (EntityJid) opt_jid, addr1, addr2,
messageListener);
PurplkiteLogs.logInfo(TAG, "chat value single chat 1 :" + chat);
} else {
chat.addMessageListener(messageListener);
PurplkiteLogs.logInfo(TAG, "chat value single chat 2:" + chat);
}
} catch (Exception e) {
e.printStackTrace();
}
GoalKicker.com – Android™ Notes for Professionals 871
XMPP.getInstance().addStanzaListener(getActivity(), packetListener);
XMPP.getInstance().addChatListener(getActivity(), chatListener);
XMPP.getInstance().getSrvDeliveryManager(getActivity());
} else {
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* user attemptLogin for xmpp
*
*/
private void attemptLogin() {
if (mAuthTask != null) {
return;
}
boolean cancel = false;
View focusView = null;
if (cancel) {
focusView.requestFocus();
} else {
try {
mAuthTask = new UserLoginTask();
mAuthTask.execute((Void) null);
} catch (Exception e) {
}
}
}
void onChatCreated(Chat chatCreated) {
if (chat != null) {
if (chat.getParticipant().getLocalpart().toString().equals(
chatCreated.getParticipant().getLocalpart().toString())) {
chat.removeMessageListener(messageListener);
chat = chatCreated;
chat.addMessageListener(messageListener);
}
} else {
chat = chatCreated;
chat.addMessageListener(messageListener);
}
}
private void sendMessage(String message) {
if (chat != null) {
try {
chat.sendMessage(message);
} catch (SmackException.NotConnectedException e) {
e.printStackTrace();
} catch (Exception e) {
GoalKicker.com – Android™ Notes for Professionals 872
e.printStackTrace();
}
}
}
@Override
public void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
try {
XMPP.getInstance().removeChatListener(getActivity(), chatListener);
if (chat != null && messageListener != null) {
XMPP.getInstance().removeStanzaListener(getActivity(), packetListener);
chat.removeMessageListener(messageListener);
}
} catch (Exception e) {
e.printStackTrace();
}
}
Make sure the internet permission is added in your manifest file.
GoalKicker.com – Android™ Notes for Professionals 873
Chapter 168: Android Authenticator
Section 168.1: Basic Account Authenticator Service
The Android Account Authenticator system can be used to make the client authenticate with a remote server. Three
pieces of information are required:
A service, triggered by the android.accounts.AccountAuthenticator. Its onBind method should return a
subclass of AbstractAccountAuthenticator.
An activity to prompt the user for credentials (Login activity)
An xml resource file to describe the account
1. The service:
Place the following permissions in your AndroidManifest.xml:
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
Declare the service in the manifest file:
<service android:name="com.example.MyAuthenticationService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
Note that the android.accounts.AccountAuthenticator is included within the intent-filter tag. The xml
resource (named authenticator here) is specified in the meta-data tag.
The service class:
public class MyAuthenticationService extends Service {
private static final Object lock = new Object();
private MyAuthenticator mAuthenticator;
public MyAuthenticationService() {
super();
}
@Override
public void onCreate() {
super.onCreate();
synchronized (lock) {
if (mAuthenticator == null) {
mAuthenticator = new MyAuthenticator(this);
}
}
}
GoalKicker.com – Android™ Notes for Professionals 874
@Override
public IBinder onBind(Intent intent) {
return mAuthenticator.getIBinder();
}
}
2. The xml resource:
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.example.account"
android:icon="@drawable/appicon"
android:smallIcon="@drawable/appicon"
android:label="@string/app_name" />
Do not directly assign a string to android:label or assign missing drawables. It will crash without warning.
3. Extend the AbstractAccountAuthenticator class:
public class MyAuthenticator extends AbstractAccountAuthenticator {
private Context mContext;
public MyAuthenticator(Context context) {
super(context);
mContext = context;
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response,
String accountType,
String authTokenType,
String[] requiredFeatures,
Bundle options) throws NetworkErrorException {
Intent intent = new Intent(mContext, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle
options) throws NetworkErrorException {
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
return null;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String
authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
GoalKicker.com – Android™ Notes for Professionals 875
@Override
public String getAuthTokenLabel(String authTokenType) {
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[]
features) throws NetworkErrorException {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String
authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
}
The addAccount() method in AbstractAccountAuthenticator class is important as this method is called when
adding an account from the "Add Account" screen in under settings.
AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE is important, as it will include the
AccountAuthenticatorResponse object that is needed to return the account keys upon successful user verification.
GoalKicker.com – Android™ Notes for Professionals 876
Chapter 169: AudioManager
Section 169.1: Requesting Transient Audio Focus
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.requestAudioFocus(audioListener, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
changedListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// You now have the audio focus and may play sound.
// When the sound has been played you give the focus back.
audioManager.abandonAudioFocus(changedListener);
}
}
}
Section 169.2: Requesting Audio Focus
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.requestAudioFocus(audioListener, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
changedListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// You now have the audio focus and may play sound.
}
else if (focusChange == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
// Handle the failure.
}
}
}
GoalKicker.com – Android™ Notes for Professionals 877
Chapter 170: AudioTrack
Section 170.1: Generate tone of a specific frequency
To play a sound of with a specific tone,we first have to create a sine wave sound.This is done in the following way.
final int duration = 10; // duration of sound
final int sampleRate = 22050; // Hz (maximum frequency is 7902.13Hz (B8))
final int numSamples = duration * sampleRate;
final double samples[] = new double[numSamples];
final short buffer[] = new short[numSamples];
for (int i = 0; i < numSamples; ++i)
{
samples[i] = Math.sin(2 * Math.PI * i / (sampleRate / note[0])); // Sine wave
buffer[i] = (short) (samples[i] * Short.MAX_VALUE); // Higher amplitude increases volume
}
Now we have to configure AudioTrack to play in accordance with the generated buffer . It is done in the following
manner
AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, buffer.length,
AudioTrack.MODE_STATIC);
Write the generated buffer and play the track
audioTrack.write(buffer, 0, buffer.length);
audioTrack.play();
Hope this helps :)
GoalKicker.com – Android™ Notes for Professionals 878
Chapter 171: Job Scheduling
Section 171.1: Basic usage
Create a new JobService
This is done by extending the JobService class and implementing/overriding the required methods onStartJob()
and onStopJob().
public class MyJobService extends JobService
{
final String TAG = getClass().getSimpleName();
@Override
public boolean onStartJob(JobParameters jobParameters) {
Log.i(TAG, "Job started");
// ... your code here ...
jobFinished(jobParameters, false); // signal that we're done and don't want to reschedule
the job
return false; // finished: no more work to be done
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
Log.w(TAG, "Job stopped");
return false;
}
}
Add the new JobService to your AndroidManifest.xml
The following step is mandatory, otherwise you won't be able to run your job:
Declare your MyJobService class as a new <service> element between <application> </application> in your
AndroidManifest.xml.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MyJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>
GoalKicker.com – Android™ Notes for Professionals 879
</manifest>
Setup and run the job
After you implemented a new JobService and added it to your AndroidManifest.xml, you can continue with the final
steps.
onButtonClick_startJob() prepares and runs a periodical job. Besides periodic jobs, JobInfo.Builder
allows to specify many other settings and constraints. For example you can define that a plugged in charger or
a network connection is required to run the job.
onButtonClick_stopJob() cancels all running jobs
public class MainActivity extends AppCompatActivity
{
final String TAG = getClass().getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onButtonClick_startJob(View v) {
// get the jobScheduler instance from current context
JobScheduler jobScheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
// MyJobService provides the implementation for the job
ComponentName jobService = new ComponentName(getApplicationContext(), MyJobService.class);
// define that the job will run periodically in intervals of 10 seconds
JobInfo jobInfo = new JobInfo.Builder(1, jobService).setPeriodic(10 * 1000).build();
// schedule/start the job
int result = jobScheduler.schedule(jobInfo);
if (result == JobScheduler.RESULT_SUCCESS)
Log.d(TAG, "Successfully scheduled job: " + result);
else
Log.e(TAG, "RESULT_FAILURE: " + result);
}
public void onButtonClick_stopJob(View v) {
JobScheduler jobScheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
Log.d(TAG, "Stopping all jobs...");
jobScheduler.cancelAll(); // cancel all potentially running jobs
}
}
After calling onButtonClick_startJob(), the job will approximately run in intervals of 10 seconds, even when the
app is in the paused state (user pressed home button and app is no longer visible).
Instead of cancelling all running jobs inside onButtonClick_stopJob(), you can also call jobScheduler.cancel() to
cancel a specific job based on it's job ID.
GoalKicker.com – Android™ Notes for Professionals 880
Chapter 172: Accounts and
AccountManager
Section 172.1: Understanding custom accounts/authentication
The following example is high level coverage of the key concepts and basic skeletal setup:
1. Collects credentials from the user (Usually from a login screen you've created)
2. Authenticates the credentials with the server (stores custom authentication)
3. Stores the credentials on the device
Extend an AbstractAccountAuthenticator (Primarily used to retrieve authentication & re-authenticate them)
public class AccountAuthenticator extends AbstractAccountAuthenticator {
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
String authTokenType, String[] requiredFeatures, Bundle options) {
//intent to start the login activity
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle
options) {
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String
authTokenType,
Bundle options) throws NetworkErrorException {
//retrieve authentication tokens from account manager storage or custom storage or reauthenticate
old tokens and return new ones
}
@Override
public String getAuthTokenLabel(String authTokenType) {
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[]
features)
throws NetworkErrorException {
//check whether the account supports certain features
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String
authTokenType,
Bundle options) {
//when the user's session has expired or requires their previously available credentials to be
updated, here is the function to do it.
GoalKicker.com – Android™ Notes for Professionals 881
}
}
Create a service (Account Manager framework connects to the extended AbstractAccountAuthenticator through the
service interface)
public class AuthenticatorService extends Service {
private AccountAuthenticator authenticator;
@Override
public void onCreate(){
authenticator = new AccountAuthenticator(this);
}
@Override
public IBinder onBind(Intent intent) {
return authenticator.getIBinder();
}
}
Authenticator XML configuration (The account manager framework requires. This is what you'll see inside Settings ->
Accounts in Android)
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="rename.with.your.applicationid"
android:icon="@drawable/app_icon"
android:label="@string/app_name"
android:smallIcon="@drawable/app_icon" />
Changes to the AndroidManifest.xml (Bring all the above concepts together to make it usable programmatically
through the AccountManager)
<application
...>
<service
android:name=".authenticator.AccountAuthenticatorService"
android:exported="false"
android:process=":authentication">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator"/>
</service>
</application>
The next example will contain how to make use of this setup.
GoalKicker.com – Android™ Notes for Professionals 882
Chapter 173: Integrate OpenCV into
Android Studio
Section 173.1: Instructions
Tested with A.S. v1.4.1 but should work with newer versions too.
1. Create a new Android Studio project using the project wizard (Menu:/File/New Project):
Call it "cvtest1"
Form factor: API 19, Android 4.4 (KitKat)
Blank Activity named MainActivity
You should have a cvtest1 directory where this project is stored. (the title bar of Android studio shows you
where cvtest1 is when you open the project)
2. Verify that your app runs correctly. Try changing something like the "Hello World" text to confirm that the
build/test cycle is OK for you. (I'm testing with an emulator of an API 19 device).
3. Download the OpenCV package for Android v3.1.0 and unzip it in some temporary directory somewhere.
(Make sure it is the package specifically for Android and not just the OpenCV for Java package.) I'll call this
directory "unzip-dir" Below unzip-dir you should have a sdk/native/libs directory with subdirectories that
start with things like arm..., mips... and x86... (one for each type of "architecture" Android runs on)
4. From Android Studio import OpenCV into your project as a module: Menu:/File/New/Import_Module:
Source-directory: {unzip-dir}/sdk/java
Module name: Android studio automatically fills in this field with openCVLibrary310 (the exact name
probably doesn't matter but we'll go with this).
Click on next. You get a screen with three checkboxes and questions about jars, libraries and import
options. All three should be checked. Click on Finish.
Android Studio starts to import the module and you are shown an import-summary.txt file that has a list of
what was not imported (mostly javadoc files) and other pieces of information.
GoalKicker.com – Android™ Notes for Professionals 883
But you also get an error message saying failed to find target with hash string 'android-14'.... This
happens because the build.gradle file in the OpenCV zip file you downloaded says to compile using android
API version 14, which by default you don't have with Android Studio v1.4.1.
5. Open the project structure dialogue (Menu:/File/Project_Structure). Select the "app" module, click on the
Dependencies tab and add :openCVLibrary310 as a Module Dependency. When you select
Add/Module_Dependency it should appear in the list of modules you can add. It will now show up as a
dependency but you will get a few more cannot-find-android-14 errors in the event log.
6. Look in the build.gradle file for your app module. There are multiple build.gradle files in an Android project.
GoalKicker.com – Android™ Notes for Professionals 884
The one you want is in the cvtest1/app directory and from the project view it looks like build.gradle
(Module: app). Note the values of these four fields:
compileSDKVersion (mine says 23)
buildToolsVersion (mine says 23.0.2)
minSdkVersion (mine says 19)
targetSdkVersion (mine says 23)
7. Your project now has a cvtest1/OpenCVLibrary310 directory but it is not visible from the project view:
Use some other tool, such as any file manager, and go to this directory. You can also switch the project view from
Android to Project Files and you can find this directory as shown in this screenshot:
Inside there is another build.gradle file (it's highlighted in the above screenshot). Update this file with the four
values from step 6.
8. Resynch your project and then clean/rebuild it. (Menu:/Build/Clean_Project) It should clean and build
GoalKicker.com – Android™ Notes for Professionals 885
without errors and you should see many references to :openCVLibrary310 in the 0:Messages screen.
At this point the module should appear in the project hierarchy as openCVLibrary310, just like app. (Note
that in that little drop-down menu I switched back from Project View to Android View ). You should also see
an additional build.gradle file under "Gradle Scripts" but I find the Android Studio interface a little bit glitchy
and sometimes it does not do this right away. So try resynching, cleaning, even restarting Android Studio.
You should see the openCVLibrary310 module with all the OpenCV functions under java like in this
screenshot:
GoalKicker.com – Android™ Notes for Professionals 886
9. Copy the {unzip-dir}/sdk/native/libs directory (and everything under it) to your Android project, to
cvtest1/OpenCVLibrary310/src/main/, and then rename your copy from libs to jniLibs. You should now
have a cvtest1/OpenCVLibrary310/src/main/jniLibs directory. Resynch your project and this directory
should now appear in the project view under openCVLibrary310.
GoalKicker.com – Android™ Notes for Professionals 887
10. Go to the onCreate method of MainActivity.java and append this code:
if (!OpenCVLoader.initDebug()) {
Log.e(this.getClass().getSimpleName(), " OpenCVLoader.initDebug(), not working.");
} else {
Log.d(this.getClass().getSimpleName(), " OpenCVLoader.initDebug(), working.");
}
Then run your application. You should see lines like this in the Android Monitor:
GoalKicker.com – Android™ Notes for Professionals 888
(I don't know why that line with the error message is there)
11. Now try to actually use some openCV code. In the example below I copied a .jpg file to the cache directory of
the cvtest1 application on the android emulator. The code below loads this image, runs the canny edge
detection algorithm and then writes the results back to a .png file in the same directory.
Put this code just below the code from the previous step and alter it to match your own
files/directories.
String inputFileName="simm_01";
String inputExtension = "jpg";
String inputDir = getCacheDir().getAbsolutePath(); // use the cache directory for i/o
String outputDir = getCacheDir().getAbsolutePath();
String outputExtension = "png";
String inputFilePath = inputDir + File.separator + inputFileName + "." + inputExtension;
Log.d (this.getClass().getSimpleName(), "loading " + inputFilePath + "...");
Mat image = Imgcodecs.imread(inputFilePath);
Log.d (this.getClass().getSimpleName(), "width of " + inputFileName + ": " + image.width());
// if width is 0 then it did not read your image.
// for the canny edge detection algorithm, play with these to see different results
int threshold1 = 70;
int threshold2 = 100;
Mat im_canny = new Mat(); // you have to initialize output image before giving it to the Canny
method
Imgproc.Canny(image, im_canny, threshold1, threshold2);
String cannyFilename = outputDir + File.separator + inputFileName + "_canny-" + threshold1 + "-
" + threshold2 + "." + outputExtension;
Log.d (this.getClass().getSimpleName(), "Writing " + cannyFilename);
Imgcodecs.imwrite(cannyFilename, im_canny);
GoalKicker.com – Android™ Notes for Professionals 889
12. Run your application. Your emulator should create a black and white "edge" image. You can use the Android
Device Monitor to retrieve the output or write an activity to show it.
GoalKicker.com – Android™ Notes for Professionals 890
Chapter 174: MVVM (Architecture)
Section 174.1: MVVM Example using DataBinding Library
The whole point of MVVM is to separate layers containing logic from the view layer.
On Android we can use the DataBinding Library to help us with this and make most of our logic Unit-testable
without worrying about Android dependencies.
In this example I'll show the central components for a stupid simple App that does the following:
At start up fake a network call and show a loading spinner
Show a view with a click counter TextView, a message TextView, and a button to increment the counter
On button click update counter and update counter color and message text if counter reaches some number
Let's start with the view layer:
activity_main.xml:
If you're unfamiliar with how DataBinding works you should probably take 10 minutes to make yourself familiar
with it. As you can see, all fields you would usually update with setters are bound to functions on the viewModel
variable.
If you've got a question about the android:visibility or app:textColor properties check the 'Remarks' section.
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="de.walled.mvvmtest.viewmodel.ClickerViewModel"/>
</data>
<RelativeLayout
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_horizontal_margin"
tools:context="de.walled.mvvmtest.view.MainActivity">
<LinearLayout
android:id="@+id/click_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="60dp"
android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"
android:padding="8dp"
android:orientation="horizontal">
GoalKicker.com – Android™ Notes for Professionals 891
<TextView
android:id="@+id/number_of_clicks"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/ClickCounter"
android:text="@{viewModel.numberOfClicks}"
android:textAlignment="center"
app:textColor="@{viewModel.counterColor}"
tools:text="8"
tools:textColor="@color/red"
/>
<TextView
android:id="@+id/static_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
style="@style/ClickCounter"
android:text="@string/label.clicks"
app:textColor="@{viewModel.counterColor}"
android:textAlignment="center"
tools:textColor="@color/red"
/>
</LinearLayout>
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/click_counter"
android:layout_centerHorizontal="true"
android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"
android:text="@{viewModel.labelText}"
android:textAlignment="center"
android:textSize="18sp"
tools:text="You're bad and you should feel bad!"
/>
<Button
android:id="@+id/clicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/message"
android:layout_centerHorizontal="true"
android:layout_marginTop="8dp"
android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"
android:padding="8dp"
android:text="@string/label.button"
android:onClick="@{() -> viewModel.onClickIncrement()}"
/>
GoalKicker.com – Android™ Notes for Professionals 892
<android.support.v4.widget.ContentLoadingProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="90dp"
android:layout_centerHorizontal="true"
style="@android:style/Widget.ProgressBar.Inverse"
android:visibility="@{viewModel.loadingVisible ? View.VISIBLE : View.GONE}"
android:indeterminate="true"
/>
</RelativeLayout>
</layout>
Next the model layer. Here I have:
two fields that represent the state of the app
getters to read the number of clicks and the state of excitement
a method to increment my click count
a method to restore some previous state (important for orientation changes)
Also I define here a 'state of excitement' that is dependent on the number of clicks. This will later be used to update
color and message on the View.
It is important to note that there are no assumptions made in the model about how the state might be displayed to
the user!
ClickerModel.java
import com.google.common.base.Optional;
import de.walled.mvvmtest.viewmodel.ViewState;
public class ClickerModel implements IClickerModel {
private int numberOfClicks;
private Excitement stateOfExcitement;
public void incrementClicks() {
numberOfClicks += 1;
updateStateOfExcitement();
}
public int getNumberOfClicks() {
return Optional.fromNullable(numberOfClicks).or(0);
}
public Excitement getStateOfExcitement() {
return Optional.fromNullable(stateOfExcitement).or(Excitement.BOO);
}
public void restoreState(ViewState state) {
numberOfClicks = state.getNumberOfClicks();
updateStateOfExcitement();
}
private void updateStateOfExcitement() {
if (numberOfClicks < 10) {
stateOfExcitement = Excitement.BOO;
GoalKicker.com – Android™ Notes for Professionals 893
} else if (numberOfClicks <= 20) {
stateOfExcitement = Excitement.MEH;
} else {
stateOfExcitement = Excitement.WOOHOO;
}
}
}
Next the ViewModel.
This will trigger changes on the model and format data from the model to show them on the view. Note that it is
here where we evaluate which GUI representation is appropriate for the state given by the model
(resolveCounterColor and resolveLabelText). So we could for example easily implement an
UnderachieverClickerModel that has lower thresholds for the state of excitement without touching any code in the
viewModel or view.
Also note that the ViewModel does not hold any references to view objects. All properties are bound via the
@Bindable annotations and updated when either notifyChange() (signals all properties need to be updated) or
notifyPropertyChanged(BR.propertyName) (signals this properties need to be updated).
ClickerViewModel.java
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.support.annotation.ColorRes;
import android.support.annotation.StringRes;
import com.android.databinding.library.baseAdapters.BR;
import de.walled.mvvmtest.R;
import de.walled.mvvmtest.api.IClickerApi;
import de.walled.mvvmtest.model.Excitement;
import de.walled.mvvmtest.model.IClickerModel;
import rx.Observable;
public class ClickerViewModel extends BaseObservable {
private final IClickerApi api;
boolean isLoading = false;
private IClickerModel model;
public ClickerViewModel(IClickerModel model, IClickerApi api) {
this.model = model;
this.api = api;
}
public void onClickIncrement() {
model.incrementClicks();
notifyChange();
}
public ViewState getViewState() {
ViewState viewState = new ViewState();
viewState.setNumberOfClicks(model.getNumberOfClicks());
return viewState;
}
public Observable<ViewState> loadData() {
isLoading = true;
GoalKicker.com – Android™ Notes for Professionals 894
return api.fetchInitialState()
.doOnNext(this::initModel)
.doOnTerminate(() -> {
isLoading = false;
notifyPropertyChanged(BR.loadingVisible);
notifyPropertyChanged(BR.contentVisible);
});
}
public void initFromSavedState(ViewState savedState) {
initModel(savedState);
}
@Bindable
public String getNumberOfClicks() {
final int clicks = model.getNumberOfClicks();
return String.valueOf(clicks);
}
@Bindable
@StringRes
public int getLabelText() {
final Excitement stateOfExcitement = model.getStateOfExcitement();
return resolveLabelText(stateOfExcitement);
}
@Bindable
@ColorRes
public int getCounterColor() {
final Excitement stateOfExcitement = model.getStateOfExcitement();
return resolveCounterColor(stateOfExcitement);
}
@Bindable
public boolean isLoadingVisible() {
return isLoading;
}
@Bindable
public boolean isContentVisible() {
return !isLoading;
}
private void initModel(final ViewState viewState) {
model.restoreState(viewState);
notifyChange();
}
@ColorRes
private int resolveCounterColor(Excitement stateOfExcitement) {
switch (stateOfExcitement) {
case MEH:
return R.color.yellow;
case WOOHOO:
return R.color.green;
default:
return R.color.red;
}
}
@StringRes
private int resolveLabelText(Excitement stateOfExcitement) {
GoalKicker.com – Android™ Notes for Professionals 895
switch (stateOfExcitement) {
case MEH:
return R.string.label_indifferent;
case WOOHOO:
return R.string.label_excited;
default:
return R.string.label_negative;
}
}
}
Tying it all together in the Activity!
Here we see the view initializing the viewModel with all dependencies it might need, that have to be instantiated
from an android context.
After the viewModel is initialized it is bound to the xml layout via the DataBindingUtil (Please check 'Syntax' section
for naming of generated classes).
Note subscriptions are subscribed to on this layer because we have to handle unsubscribing them when the activity
is paused or destroyed to avoid memory leaks and NPEs. Also persisting and reloading of the viewState on
OrientationChanges is triggered here
MainActivity.java
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import de.walled.mvvmtest.R;
import de.walled.mvvmtest.api.ClickerApi;
import de.walled.mvvmtest.api.IClickerApi;
import de.walled.mvvmtest.databinding.ActivityMainBinding;
import de.walled.mvvmtest.model.ClickerModel;
import de.walled.mvvmtest.viewmodel.ClickerViewModel;
import de.walled.mvvmtest.viewmodel.ViewState;
import rx.Subscription;
import rx.subscriptions.Subscriptions;
public class MainActivity extends AppCompatActivity {
private static final String KEY_VIEW_STATE = "state.view";
private ClickerViewModel viewModel;
private Subscription fakeLoader = Subscriptions.unsubscribed();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// would usually be injected but I feel Dagger would be out of scope
final IClickerApi api = new ClickerApi();
setupViewModel(savedInstanceState, api);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setViewModel(viewModel);
}
@Override
GoalKicker.com – Android™ Notes for Professionals 896
protected void onPause() {
fakeLoader.unsubscribe();
super.onPause();
}
@Override
protected void onDestroy() {
fakeLoader.unsubscribe();
super.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putSerializable(KEY_VIEW_STATE, viewModel.getViewState());
}
private void setupViewModel(Bundle savedInstance, IClickerApi api) {
viewModel = new ClickerViewModel(new ClickerModel(), api);
final ViewState savedState = getViewStateFromBundle(savedInstance);
if (savedState == null) {
fakeLoader = viewModel.loadData().subscribe();
} else {
viewModel.initFromSavedState(savedState);
}
}
private ViewState getViewStateFromBundle(Bundle savedInstance) {
if (savedInstance != null) {
return (ViewState) savedInstance.getSerializable(KEY_VIEW_STATE);
}
return null;
}
}
To see everything in action check out this example project.
GoalKicker.com – Android™ Notes for Professionals 897
Chapter 175: ORMLite in android
Section 175.1: Android OrmLite over SQLite example
ORMLite is an Object Relational Mapping package that provides simple and lightweight functionality for persisting
Java objects to SQL databases while avoiding the complexity and overhead of more standard ORM packages.
Speaking for Android, OrmLite is implemented over the out-of-the-box supported database, SQLite. It makes direct
calls to the API to access SQLite.
Gradle setup
To get started you should include the package to the build gradle.
// https://mvnrepository.com/artifact/com.j256.ormlite/ormlite-android
compile group: 'com.j256.ormlite', name: 'ormlite-android', version: '5.0'
POJO configuration
Then you should configure a POJO to be persisted to the database. Here care must be taken to the annotations:
Add the @DatabaseTable annotation to the top of each class. You can also use @Entity.
Add the @DatabaseField annotation right before each field to be persisted. You can also use @Column and
others.
Add a no-argument constructor to each class with at least package visibility.
@DatabaseTable(tableName = "form_model")
public class FormModel implements Serializable {
@DatabaseField(generatedId = true)
private Long id;
@DatabaseField(dataType = DataType.SERIALIZABLE)
ArrayList<ReviewItem> reviewItems;
@DatabaseField(index = true)
private String username;
@DatabaseField
private String createdAt;
public FormModel() {
}
public FormModel(ArrayList<ReviewItem> reviewItems, String username, String createdAt) {
this.reviewItems = reviewItems;
this.username = username;
this.createdAt = createdAt;
}
}
At the example above there is one table (form_model) with 4 fields.
id field is auto generated index.
username is an index to the database.
More information about the annotation can be found at the official documentation.
GoalKicker.com – Android™ Notes for Professionals 898
Database Helper
To continue with, you will need to create a database helper class which should extend the
OrmLiteSqliteOpenHelper class.
This class creates and upgrades the database when your application is installed and can also provide the DAO
classes used by your other classes.
DAO stands for Data Access Object and it provides all the scrum functionality and specializes in the handling a
single persisted class.
The helper class must implement the following two methods:
onCreate(SQLiteDatabase sqliteDatabase, ConnectionSource connectionSource);
onCreate creates the database when your app is first installed
onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion);
onUpgrade handles the upgrading of the database tables when you upgrade your app to a new version
Database Helper class example:
public class OrmLite extends OrmLiteSqliteOpenHelper {
//Database name
private static final String DATABASE_NAME = "gaia";
//Version of the database. Changing the version will call {@Link OrmLite.onUpgrade}
private static final int DATABASE_VERSION = 2;
/**
* The data access object used to interact with the Sqlite database to do C.R.U.D operations.
*/
private Dao<FormModel, Long> todoDao;
public OrmLite(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION,
/**
* R.raw.ormlite_config is a reference to the ormlite_config2.txt file in the
* /res/raw/ directory of this project
* */
R.raw.ormlite_config2);
}
@Override
public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
try {
/**
* creates the database table
*/
TableUtils.createTable(connectionSource, FormModel.class);
} catch (SQLException e) {
GoalKicker.com – Android™ Notes for Professionals 899
e.printStackTrace();
} catch (java.sql.SQLException e) {
e.printStackTrace();
}
}
/*
It is called when you construct a SQLiteOpenHelper with version newer than the version of
the opened database.
*/
@Override
public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource,
int oldVersion, int newVersion) {
try {
/**
* Recreates the database when onUpgrade is called by the framework
*/
TableUtils.dropTable(connectionSource, FormModel.class, false);
onCreate(database, connectionSource);
} catch (SQLException | java.sql.SQLException e) {
e.printStackTrace();
}
}
/**
* Returns an instance of the data access object
* @return
* @throws SQLException
*/
public Dao<FormModel, Long> getDao() throws SQLException {
if(todoDao == null) {
try {
todoDao = getDao(FormModel.class);
} catch (java.sql.SQLException e) {
e.printStackTrace();
}
}
return todoDao;
}
}
Persisting Object to SQLite
Finally, the class that persists the object to the database.
public class ReviewPresenter {
Dao<FormModel, Long> simpleDao;
public ReviewPresenter(Application application) {
this.application = (GaiaApplication) application;
simpleDao = this.application.getHelper().getDao();
}
public void storeFormToSqLite(FormModel form) {
try {
simpleDao.create(form);
} catch (SQLException e) {
e.printStackTrace();
}
List<FormModel> list = null;
GoalKicker.com – Android™ Notes for Professionals 900
try {
// query for all of the data objects in the database
list = simpleDao.queryForAll();
} catch (SQLException e) {
e.printStackTrace();
}
// our string builder for building the content-view
StringBuilder sb = new StringBuilder();
int simpleC = 1;
for (FormModel simple : list) {
sb.append('#').append(simpleC).append(": ").append(simple.getUsername()).append('\n');
simpleC++;
}
System.out.println(sb.toString());
}
//Query to database to get all forms by username
public List<FormModel> getAllFormsByUsername(String username) {
List<FormModel> results = null;
try {
results = simpleDao.queryBuilder().where().eq("username",
PreferencesManager.getInstance().getString(Constants.USERNAME)).query();
} catch (SQLException e) {
e.printStackTrace();
}
return results;
}
}
The accessor of the DOA at the constructor of the above class is defined as:
private OrmLite dbHelper = null;
/*
Provides the SQLite Helper Object among the application
*/
public OrmLite getHelper() {
if (dbHelper == null) {
dbHelper = OpenHelperManager.getHelper(this, OrmLite.class);
}
return dbHelper;
}
GoalKicker.com – Android™ Notes for Professionals 901
Chapter 176: Retrofit2 with RxJava
Section 176.1: Retrofit2 with RxJava
First, add relevant dependencies into the build.gradle file.
dependencies {
....
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0'
....
}
Then create the model you would like to receive:
public class Server {
public String name;
public String url;
public String apikey;
public List<Site> siteList;
}
Create an interface containing methods used to exchange data with remote server:
public interface ApiServerRequests {
@GET("api/get-servers")
public Observable<List<Server>> getServers();
}
Then create a Retrofit instance:
public ApiRequests DeviceAPIHelper ()
{
Gson gson = new GsonBuilder().create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://example.com/")
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
api = retrofit.create(ApiServerRequests.class);
return api;
}
Then, anywhere from the code, call the method:
apiRequests.getServers()
.subscribeOn(Schedulers.io()) // the observable is emitted on io thread
.observerOn(AndroidSchedulers.mainThread()) // Methods needed to handle request in background
thread
.subscribe(new Subscriber<List<Server>>() {
@Override
public void onCompleted() {
GoalKicker.com – Android™ Notes for Professionals 902
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(List<Server> servers) {
//A list of servers is fetched successfully
}
});
Section 176.2: Nested requests example: multiple requests,
combine results
Suppose we have an API which allows us to get object metadata in single request (getAllPets), and other request
which have full data of single resource (getSinglePet). How we can query all of them in a single chain?
public class PetsFetcher {
static class PetRepository {
List<Integer> ids;
}
static class Pet {
int id;
String name;
int weight;
int height;
}
interface PetApi {
@GET("pets") Observable<PetRepository> getAllPets();
@GET("pet/{id}") Observable<Pet> getSinglePet(@Path("id") int id);
}
PetApi petApi;
Disposable petsDisposable;
public void requestAllPets() {
petApi.getAllPets()
.doOnSubscribe(new Consumer<Disposable>() {
@Override public void accept(Disposable disposable) throws Exception {
petsDisposable = disposable;
}
})
.flatMap(new Function<PetRepository, ObservableSource<Integer>>() {
@Override
public ObservableSource<Integer> apply(PetRepository petRepository) throws
Exception {
List<Integer> petIds = petRepository.ids;
return Observable.fromIterable(petIds);
}
GoalKicker.com – Android™ Notes for Professionals 903
})
.flatMap(new Function<Integer, ObservableSource<Pet>>() {
@Override public ObservableSource<Pet> apply(Integer id) throws Exception {
return petApi.getSinglePet(id);
}
})
.toList()
.toObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<Pet>>() {
@Override public void accept(List<Pet> pets) throws Exception {
//use your pets here
}
}, new Consumer<Throwable>() {
@Override public void accept(Throwable throwable) throws Exception {
//show user something goes wrong
}
});
}
void cancelRequests(){
if (petsDisposable!=null){
petsDisposable.dispose();
petsDisposable = null;
}
}
}
Section 176.3: Retrofit with RxJava to fetch data
asyncronously
From the GitHub repo of RxJava, RxJava is a Java VM implementation of Reactive Extensions: a library for composing
asynchronous and event-based programs by using observable sequences. It extends the observer pattern to support
sequences of data/events and adds operators that allow you to compose sequences together declaratively while
abstracting away concerns about things like low-level threading, synchronisation, thread-safety and concurrent data
structures.
Retrofit is a type-safe HTTP client for Android and Java, using this, developers can make all network stuff much
more easier. As an example, we are going to download some JSON and show it in RecyclerView as a list.
Getting started:
Add RxJava, RxAndroid and Retrofit dependencies in your app level build.gradle file: compile
"io.reactivex:rxjava:1.1.6"
compile "io.reactivex:rxandroid:1.2.1"
compile "com.squareup.retrofit2:adapter-rxjava:2.0.2"
compile "com.google.code.gson:gson:2.6.2"
compile "com.squareup.retrofit2:retrofit:2.0.2"
compile "com.squareup.retrofit2:converter-gson:2.0.2"
Define ApiClient and ApiInterface to exchange data from server
public class ApiClient {
private static Retrofit retrofitInstance = null;
private static final String BASE_URL = "https://api.github.com/";
GoalKicker.com – Android™ Notes for Professionals 904
public static Retrofit getInstance() {
if (retrofitInstance == null) {
retrofitInstance = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofitInstance;
}
public static <T> T createRetrofitService(final Class<T> clazz, final String endPoint) {
final Retrofit restAdapter = new Retrofit.Builder()
.baseUrl(endPoint)
.build();
return restAdapter.create(clazz);
}
public static String getBaseUrl() {
return BASE_URL;
}}
public interface ApiInterface {
@GET("repos/{org}/{repo}/issues")
Observable<List<Issue>> getIssues(@Path("org") String organisation,
@Path("repo") String repositoryName,
@Query("page") int pageNumber);}
Note the getRepos() is returning an Observable and not just a list of issues.
Define the models
An example for this is shown. You can use free services like JsonSchema2Pojo or this.
public class Comment {
@SerializedName("url")
@Expose
private String url;
@SerializedName("html_url")
@Expose
private String htmlUrl;
//Getters and Setters
}
Create Retrofit instance
ApiInterface apiService = ApiClient.getInstance().create(ApiInterface.class);
Then, Use this instance to fetch data from server
Observable<List<Issue>> issueObservable = apiService.getIssues(org, repo,
pageNumber);
issueObservable.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.map(issues -> issues) //get issues and map to issues list
.subscribe(new Subscriber<List<Issue>>() {
GoalKicker.com – Android™ Notes for Professionals 905
@Override
public void onCompleted() {
Log.i(TAG, "onCompleted: COMPLETED!");
}
@Override
public void onError(Throwable e) {
Log.e(TAG, "onError: ", e);
}
@Override
public void onNext(List<Issue> issues) {
recyclerView.setAdapter(new IssueAdapter(MainActivity.this, issues,
apiService));
}
});
Now, you have successfully fetched data from a server using Retrofit and RxJava.
GoalKicker.com – Android™ Notes for Professionals 906
Chapter 177: ShortcutManager
Section 177.1: Dynamic Launcher Shortcuts
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "id1")
.setShortLabel("Web site") // Shortcut Icon tab
.setLongLabel("Open the web site") // Displayed When Long Pressing On App Icon
.setIcon(Icon.createWithResource(context, R.drawable.icon_website))
.setIntent(new Intent(Intent.ACTION_VIEW,
Uri.parse("https://www.mysite.example.com/")))
.build();
shortcutManager.setDynamicShortcuts(Arrays.asList(shortcut));
We can remove all dynamic shortcuts easily by calling:
shortcutManager.removeAllDynamicShortcuts();
We can update existing Dynamic Shorcuts by Using
shortcutManager.updateShortcuts(Arrays.asList(shortcut);
Please note that setDynamicShortcuts(List)is used to redefine the entire list of dynamic shortcuts,
addDynamicShortcuts(List) is used to add dynamic shortcuts to existing list of dynamic shortcuts
GoalKicker.com – Android™ Notes for Professionals 907
Chapter 178: LruCache
Section 178.1: Adding a Bitmap(Resource) to the cache
To add a resource to the cache you must provide a key and the resource. First make sure that the value is not in the
cache already
public void addResourceToMemoryCache(String key, Bitmap resource) {
if (memoryCache.get(key) == null)
memoryCache.put(key, resource);
}
Section 178.2: Initialising the cache
The Lru Cache will store all the added resources (values) for fast access until it reaches a memory limit, in which
case it will drop the less used resource (value) to store the new one.
To initialise the Lru cache you need to provide a maximum memory value. This value depends on your application
requirements and in how critical the resource is to keep a smooth app usage. A recommended value for an image
gallery, for example, would be 1/8 of your maximum available memory.
Also note that the Lru Cache works on a key-value basis. In the following example, the key is a String and the value
is a Bitmap:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> = memoryCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount();
}
};
Section 178.3: Getting a Bitmap(Resouce) from the cache
To get a resource from the cache simply pass the key of your resource (String in this example)
public Bitmap getResourceFromMemoryCache(String key) {
memoryCache.get(key);
}
GoalKicker.com – Android™ Notes for Professionals 908
Chapter 179: Jenkins CI setup for Android
Projects
Section 179.1: Step by step approach to set up Jenkins for
Android
This is a step by step guide to set up the automated build process using Jenkins CI for your Android projects. The
following steps assume that you have new hardware with just any flavor of Linux installed. It is also taken into
account that you might have a remote machine.
PART I: Initial setup on your machine
1. Log in via ssh to your Ubuntu machine:
ssh username@xxx.xxx.xxx
2. Download a version of the Android SDK on your machine:
wget https://dl.google.com/android/android-sdk_r24.4.1-linux.tgz
3. Unzip the downloaded tar file:
sudo apt-get install tar
tar -xvf android-sdk_r24.4.1-linux.tgz
4. Now you need to install Java 8 on your Ubuntu machine, which is a requirement for Android builds on
Nougat. Jenkins would require you to install JDK and JRE 7 using the steps below:
sudo apt-get install python-software-properties
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
apt-get install openjdk-8-jdk
5. Now install Jenkins on your Ubuntu machine:
wget -q -O - https://pkg.jenkins.io/debian/jenkins-ci.org.key | sudo apt-key add -
sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
sudo apt-get update
sudo apt-get install jenkins
GoalKicker.com – Android™ Notes for Professionals 909
6. Download the latest supported Gradle version for your Android setup:
wget https://services.gradle.org/distributions/gradle-2.14.1-all.zip
unzip gradle-2.14.1-all.zip
7. Set up Android on your Ubuntu machine. First move to the tools folder in the Android SDK folder
downloaded in step 2:
cd android-sdk-linux/tools // lists available SDK
android update sdk --no-ui // Updates SDK version
android list sdk -a | grep "SDK Build-tools" // lists available build tools
android update sdk -a -u -t 4 // updates build tools version to one listed as 4 by prev. cmd.
update java
8. Install Git or any other VCS on your machine:
sudo apt-get install git
9. Now log in to Jenkins using your internet browser. Type ipAddress:8080 into the address bar.
10. In order to receive the password for the first-time login, please check the corresponding file as follows (you
will need su permissions to access this file):
cat /var/lib/jenkins/secrets/initialAdminPassword
PART II: Set up Jenkins to build Android Jobs
1. Once logged in, go to the following path:
Jenkins > Manage Jenkins > Global Tool Configuration
2. At this location, add JAVA_HOME with the following entries:
Name = JAVA_HOME
JAVA_HOME = /usr/lib/jvm/java-8-openjdk-amd64
3. Also add the following values to Git and save the environment variables:
GoalKicker.com – Android™ Notes for Professionals 910
Name = Default
/usr/bin/git
4. Now go to the following path:
Jenkins > Manage Jenkins > Configuration
5. At this location, add ANDROID_HOME to the "global properties":
Name = ANDROID_HOME
Value = /home/username/android-sdk-linux
Part III: Create a Jenkins Job for your Android project
1. Click on New Item in the Jenkins home screen.
2. Add a Project Name and Description.
3. In the General tab, select Advanced. Then select Use custom workspace:
Directory /home/user/Code/ProjectFolder
4. In the source code management select Git. I am using Bitbucket for the purpose of this example:
Repository URL = https://username:password@bitbucket.org/project/projectname.git
5. Select additional behaviors for your repository:
Clean Before Checkout
Checkout to a sub-directory. Local subdirectory for repo /home/user/Code/ProjectFolder
6. Select a branch you want to build:
*/master
7. In the Build tab, select Execute Shell in Add build step.
8. In the Execute shell, add the following command:
GoalKicker.com – Android™ Notes for Professionals 911
cd /home/user/Code/ProjectFolder && gradle clean assemble --no-daemon
9. If you want to run Lint on the project, then add another build step into the Execute shell:
/home/user/gradle/gradle-2.14.1/bin/gradle lint
Now your system is finally set up to build Android projects using Jenkins. This setup makes your life so much easier
for releasing builds to QA and UAT teams.
PS: Since Jenkins is a different user on your Ubuntu machine, you should give it rights to create folders in your
workspace by executing the following command:
chown -R jenkins .git
GoalKicker.com – Android™ Notes for Professionals 912
Chapter 180: fastlane
Section 180.1: Fastfile lane to build and install all flavors for
given build type to a device
Add this lane to your Fastfile and run fastlane installAll type:{BUILD_TYPE} in command line. Replace
BUILD_TYPE with the build type you want to build.
For example: fastlane installAll type:Debug
This command will build all flavors of given type and install it to your device. Currently, it doesn't work if you have
more than one device attached. Make sure you have only one. In the future I'm planning to add option to select
target device.
lane :installAll do |options|
gradle(task: "clean")
gradle(task: "assemble",
build_type: options[:type])
lane_context[SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS].each do | apk |
puts "Uploading APK to Device: " + apk
begin
adb(
command: "install -r #{apk}"
)
rescue => ex
puts ex
end
end
end
Section 180.2: Fastfile to build and upload multiple flavors to
Beta by Crashlytics
This is a sample Fastfile setup for a multi-flavor app. It gives you an option to build and deploy all flavors or a single
flavor. After the deployment, it reports to Slack the status of the deployment, and sends a notification to testers in
Beta by Crashlytics testers group.
To build and deploy all flavors use:
fastlane android beta
To build a single APK and deploy use:
fastlane android beta app:flavorName
Using a single Fastlane file, you can manage iOS, Android, and Mac apps. If you are using this file just for one app
platform is not required.
How It Works
GoalKicker.com – Android™ Notes for Professionals 913
1. android argument tells fastlane that we will use :android platform.
2. Inside :android platform you can have multiple lanes. Currently, I have only :beta lane. The second
argument from the command above specifies the lane we want to use.
3. options[:app]
4. There are two Gradle tasks. First, it runs gradle clean. If you provided a flavor with app key, fastfile runs
gradle assembleReleaseFlavor. Otherwise, it runs gradle assembleRelease to build all build flavors.
5. If we are building for all flavors, an array of generated APK file names is stored inside
SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS. We use this to loop through generated files and deploy
them to Beta by Crashlytics. notifications and groups fields are optional. They are used to notify testers
registered for the app on Beta by Crashlytics.
6. If you are familiar with Crashlytics, you might know that to activate an app in the portal, you have to run it on
a device and use it first. Otherwise, Crashlytics will assume the app inactive and throw an error. In this
scenario, I capture it and report to Slack as a failure, so you will know which app is inactive.
7. If deployment is successful, fastlane will send a success message to Slack.
8. #{/([^\/]*)$/.match(apk)} this regex is used to get flavor name from APK path. You can remove it if it does
not work for you.
9. get_version_name and get_version_code are two Fastlane plugins to retrieve app version name and code.
You have to install these gems if you want to use, or you can remove them. Read more about Plugins here.
10. The else statement will be executed if you are building and deploying a single APK. We don't have to provide
apk_path to Crashlytics since we have only one app.
11. error do block at the end is used to get notified if anything else goes wrong during execution.
Note
Don't forget to replace SLACK_URL, API_TOKEN, GROUP_NAME and BUILD_SECRET with your own credentials.
fastlane_version "1.46.1"
default_platform :android
platform :android do
before_all do
ENV["SLACK_URL"] = "https://hooks.slack.com/servic...."
end
lane :beta do |options|
# Clean and build the Release version of the app.
# Usage `fastlane android beta app:flavorName`
gradle(task: "clean")
gradle(task: "assemble",
build_type: "Release",
flavor: options[:app])
# If user calls `fastlane android beta` command, it will build all projects and push them
to Crashlytics
if options[:app].nil?
lane_context[SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS].each do | apk |
puts "Uploading APK to Crashlytics: " + apk
begin
crashlytics(
api_token: "[API_TOKEN]",
build_secret: "[BUILD_SECRET]",
GoalKicker.com – Android™ Notes for Professionals 914
groups: "[GROUP_NAME]",
apk_path: apk,
notifications: "true"
)
slack(
message: "Successfully deployed new build for #{/([^\/]*)$/.match(apk)}
#{get_version_name} - #{get_version_code}",
success: true,
default_payloads: [:git_branch, :lane, :test_result]
)
rescue => ex
# If the app is inactive in Crashlytics, deployment will fail. Handle it here
and report to slack
slack(
message: "Error uploading => #{/([^\/]*)$/.match(apk)} #{get_version_name}
- #{get_version_code}: #{ex}",
success: false,
default_payloads: [:git_branch, :lane, :test_result]
)
end
end
after_all do |lane|
# This block is called, only if the executed lane was successful
slack(
message: "Operation completed for
#{lane_context[SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS].size} app(s) for #{get_version_name} -
#{get_version_code}",
default_payloads: [:git_branch, :lane, :test_result],
success: true
)
end
else
# Single APK upload to Beta by Crashlytics
crashlytics(
api_token: "[API_TOKEN]",
build_secret: "[BUILD_SECRET]",
groups: "[GROUP_NAME]",
notifications: "true"
)
after_all do |lane|
# This block is called, only if the executed lane was successful
slack(
message: "Successfully deployed new build for #{options[:app]}
#{get_version_name} - #{get_version_code}",
default_payloads: [:git_branch, :lane, :test_result],
success: true
)
end
end
error do |lane, exception|
slack(
message: exception.message,
success: false,
default_payloads: [:git_branch, :lane, :test_result]
)
end
end
end
GoalKicker.com – Android™ Notes for Professionals 915
Chapter 181: Define step value (increment)
for custom RangeSeekBar
A customization of the Android RangeSeekBar proposed by Alex Florescu at
https://github.com/anothem/android-range-seek-bar
It allows to define a step value (increment), when moving the seek bar
Section 181.1: Define a step value of 7
<RangeSeekBar
android:id="@+id/barPrice"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
app:barHeight="0.2dp"
app:barHeight2="4dp"
app:increment="7"
app:showLabels="false" />
GoalKicker.com – Android™ Notes for Professionals 916
Chapter 182: Getting started with OpenGL
ES 2.0+
This topic is about setting up and using OpenGL ES 2.0+ on Android. OpenGL ES is the standard for 2D and 3D
accelerated graphics on embedded systems - including consoles, smartphones, appliances and vehicles.
Section 182.1: Setting up GLSurfaceView and OpenGL ES 2.0+
To use OpenGL ES in your application you must add this to the manifest:
<uses-feature android:glEsVersion="0x00020000" android:required="true"/>
Create your extended GLSurfaceView:
import static android.opengl.GLES20.*; // To use all OpenGL ES 2.0 methods and constants statically
public class MyGLSurfaceView extends GLSurfaceView {
public MyGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
setEGLContextClientVersion(2); // OpenGL ES version 2.0
setRenderer(new MyRenderer());
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
public final class MyRenderer implements GLSurfaceView.Renderer{
public final void onSurfaceCreated(GL10 unused, EGLConfig config) {
// Your OpenGL ES init methods
glClearColor(1f, 0f, 0f, 1f);
}
public final void onSurfaceChanged(GL10 unused, int width, int height) {
glViewport(0, 0, width, height);
}
public final void onDrawFrame(GL10 unused) {
// Your OpenGL ES draw methods
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
}
}
Add MyGLSurfaceView to your layout:
<com.example.app.MyGLSurfaceView
android:id="@+id/gles_renderer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
To use newer version of OpenGL ES just change the version number in your manifest, in the static import and
change setEGLContextClientVersion.
Section 182.2: Compiling and Linking GLSL-ES Shaders from
GoalKicker.com – Android™ Notes for Professionals 917
asset file
The Assets folder is the most common place to store your GLSL-ES shader files. To use them in your OpenGL ES
application you need to load them to a string in the first place. This functions creates a string from the asset file:
private String loadStringFromAssetFile(Context myContext, String filePath){
StringBuilder shaderSource = new StringBuilder();
try {
BufferedReader reader = new BufferedReader(new
InputStreamReader(myContext.getAssets().open(filePath)));
String line;
while((line = reader.readLine()) != null){
shaderSource.append(line).append("\n");
}
reader.close();
return shaderSource.toString();
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "Could not load shader file");
return null;
}
}
Now you need to create a function that compiles a shader stored in a sting:
private int compileShader(int shader_type, String shaderString){
// This compiles the shader from the string
int shader = glCreateShader(shader_type);
glShaderSource(shader, shaderString);
glCompileShader(shader);
// This checks for for compilation errors
int[] compiled = new int[1];
glGetShaderiv(shader, GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
String log = glGetShaderInfoLog(shader);
Log.e(TAG, "Shader compilation error: ");
Log.e(TAG, log);
}
return shader;
}
Now you can load, compile and link your shaders:
// Load shaders from file
String vertexShaderString = loadStringFromAssetFile(context, "your_vertex_shader.glsl");
String fragmentShaderString = loadStringFromAssetFile(context, "your_fragment_shader.glsl");
// Compile shaders
int vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderString);
int fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderString);
// Link shaders and create shader program
int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram , vertexShader);
glAttachShader(shaderProgram , fragmentShader);
glLinkProgram(shaderProgram);
GoalKicker.com – Android™ Notes for Professionals 918
// Check for linking errors:
int linkStatus[] = new int[1];
glGetProgramiv(shaderProgram, GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GL_TRUE) {
String log = glGetProgramInfoLog(shaderProgram);
Log.e(TAG,"Could not link shader program: ");
Log.e(TAG, log);
}
If there are no errors, your shader program is ready to use:
glUseProgram(shaderProgram);
GoalKicker.com – Android™ Notes for Professionals 919
Chapter 183: Check Data Connection
Section 183.1: Check data connection
This method is to check data connection by ping certain IP or Domain name.
public Boolean isDataConnected() {
try {
Process p1 = java.lang.Runtime.getRuntime().exec("ping -c 1 8.8.8.8");
int returnVal = p1.waitFor();
boolean reachable = (returnVal==0);
return reachable;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
Section 183.2: Check connection using ConnectivityManager
public static boolean isConnectedNetwork (Context context) {
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
return cm.getActiveNetworkInfo () != null && cm.getActiveNetworkInfo
().isConnectedOrConnecting ();
}
Section 183.3: Use network intents to perform tasks while
data is allowed
When your device connects to a network, an intent is sent. Many apps don’t check for these intents, but to make
your application work properly, you can listen to network change intents that will tell you when communication is
possible. To check for network connectivity you can, for example, use the following clause:
if (intent.getAction().equals(android.net.ConnectivityManager.CONNECTIVITY_ACTION)){
NetworkInfo info = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
//perform your action when connected to a network
}
GoalKicker.com – Android™ Notes for Professionals 920
Chapter 184: Java on Android
Android supports all Java 7 language features and a subset of Java 8 language features that vary by platform
version. This page describes the new language features you can use, how to properly configure your project to use
them and any known issues you may encounter.
Section 184.1: Java 8 features subset with Retrolambda
Retrolambda lets you run Java 8 code with lambda expressions, method references and try-with-resources
statements on Java 7, 6 or 5. It does this by transforming your Java 8 compiled bytecode so that it can run on an
older Java runtime.
Backported Language Features:
Lambda expressions are backported by converting them to anonymous inner classes. This includes the
optimisation of using a singleton instance for stateless lambda expressions to avoid repeated object
allocation. Method references are basically just syntax sugar for lambda expressions and they are
backported in the same way.
Try-with-resources statements are backported by removing calls to Throwable.addSuppressed if the target
bytecode version is below Java 7. If you would like the suppressed exceptions to be logged instead of
swallowed, please create a feature request and we'll make it configurable.
Objects.requireNonNull calls are replaced with calls to Object.getClass if the target bytecode version is
below Java 7. The synthetic null checks generated by JDK 9 use Objects.requireNonNull, whereas earlier JDK
versions used Object.getClass.
Optionally also:
1. Default methods are backported by copying the default methods to a companion class (interface name
+ "$") as static methods, replacing the default methods in the interface with abstract methods, and by
adding the necessary method implementations to all classes which implement that interface.
2. Static methods on interfaces are backported by moving the static methods to a companion class
(interface name + "$"), and by changing all methods calls to call the new method location.
Known Limitations:
Does not backport Java 8 APIs
Backporting default methods and static methods on interfaces requires all backported interfaces and all
classes which implement them or call their static methods to be backported together, with one execution of
Retrolambda. In other words, you must always do a clean build. Also, backporting default methods won't
work across module or dependency boundaries.
May break if a future JDK 8 build stops generating a new class for each invokedynamic call. Retrolambda
works so that it captures the bytecode that java.lang.invoke.LambdaMetafactory generates dynamically, so
optimisations to that mechanism may break Retrolambda.
Retrolambda gradle plugin will automatically build your android project with Retrolambda. The latest version can be
found on the releases page.
Usage:
GoalKicker.com – Android™ Notes for Professionals 921
1. Download and install jdk8
2. Add the following to your build.gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'me.tatarka:gradle-retrolambda:<latest version>'
}
}
// Required because retrolambda is on maven central
repositories {
mavenCentral()
}
apply plugin: 'com.android.application' //or apply plugin: 'java'
apply plugin: 'me.tatarka.retrolambda'
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
Known Issues:
Lint fails on java files that have lambdas. Android's lint doesn't understand java 8 syntax and will fail silently
or loudly. There is now an experimental fork that fixes the issue.
Using Google Play Services causes Retrolambda to fail. Version 5.0.77 contains bytecode that is incompatible
with Retrolambda. This should be fixed in newer versions of play services, if you can update, that should be
the preferred solution. To work around this issue, you can either use an earlier version like 4.4.52 or add -
noverify to the jvm args.
retrolambda {
jvmArgs '-noverify'
}
GoalKicker.com – Android™ Notes for Professionals 922
Chapter 185: Android Java Native
Interface (JNI)
JNI (Java Native Interface) is a powerful tool that enables Android developers to utilize the NDK and use C++ native
code in their applications. This topic describes the usage of Java <-> C++ interface.
Section 185.1: How to call functions in a native library via the
JNI interface
The Java Native Interface (JNI) allows you to call native functions from Java code, and vice versa. This example shows
how to load and call a native function via JNI, it does not go into accessing Java methods and fields from native code
using JNI functions.
Suppose you have a native library named libjniexample.so in the project/libs/<architecture> folder, and you
want to call a function from the JNITestJava class inside the com.example.jniexample package.
In the JNITest class, declare the function like this:
public native int testJNIfunction(int a, int b);
In your native code, define the function like this:
#include <jni.h>
JNIEXPORT jint JNICALL Java_com_example_jniexample_JNITest_testJNIfunction(JNIEnv *pEnv, jobject
thiz, jint a, jint b)
{
return a + b;
}
The pEnv argument is a pointer to the JNI environment that you can pass to JNI functions to access methods and
fields of Java objects and classes. The thiz pointer is a jobject reference to the Java object that the native method
was called on (or the class if it is a static method).
In your Java code, in JNITest, load the library like this:
static{
System.loadLibrary("jniexample");
}
Note the lib at the start, and the .so at the end of the filename are omitted.
Call the native function from Java like this:
JNITest test = new JNITest();
int c = test.testJNIfunction(3, 4);
Section 185.2: How to call a Java method from native code
The Java Native Interface (JNI) allows you to call Java functions from native code. Here is a simple example of how to
do it:
Java code:
GoalKicker.com – Android™ Notes for Professionals 923
package com.example.jniexample;
public class JNITest {
public static int getAnswer(bool) {
return 42;
}
}
Native code:
int getTheAnswer()
{
// Get JNI environment
JNIEnv *env = JniGetEnv();
// Find the Java class - provide package ('.' replaced to '/') and class name
jclass jniTestClass = env->FindClass("com/example/jniexample/JNITest");
// Find the Java method - provide parameters inside () and return value (see table below for an
explanation of how to encode them)
jmethodID getAnswerMethod = env->GetStaticMethodID(jniTestClass, "getAnswer", "(Z)I;");
// Calling the method
return (int)env->CallStaticObjectMethod(jniTestClass, getAnswerMethod, (jboolean)true);
}
JNI method signature to Java type:
JNI Signature Java Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double
L fully-qualified-class ; fully-qualified-class
[ type type[]
So for our example we used (Z)I - which means the function gets a boolean and returns an int.
Section 185.3: Utility method in JNI layer
This method will help to get the Java string from C++ string.
jstring getJavaStringFromCPPString(JNIEnv *global_env, const char* cstring) {
jstring nullString = global_env->NewStringUTF(NULL);
if (!cstring) {
return nullString;
}
jclass strClass = global_env->FindClass("java/lang/String");
jmethodID ctorID = global_env->GetMethodID(strClass, "<init>",
"([BLjava/lang/String;)V");
GoalKicker.com – Android™ Notes for Professionals 924
jstring encoding = global_env->NewStringUTF("UTF-8");
jbyteArray bytes = global_env->NewByteArray(strlen(cstring));
global_env->SetByteArrayRegion(bytes, 0, strlen(cstring), (jbyte*) cstring);
jstring str = (jstring) global_env->NewObject(strClass, ctorID, bytes,
encoding);
global_env->DeleteLocalRef(strClass);
global_env->DeleteLocalRef(encoding);
global_env->DeleteLocalRef(bytes);
return str;
}
This method will help you to convert jbyteArray to char
char* as_unsigned_char_array(JNIEnv *env, jbyteArray array) {
jsize length = env->GetArrayLength(array);
jbyte* buffer = new jbyte[length + 1];
env->GetByteArrayRegion(array, 0, length, buffer);
buffer[length] = '\0';
return (char*) buffer;
}
GoalKicker.com – Android™ Notes for Professionals 925
Chapter 186: Notification Channel Android
O
Method Description
IMPORTANCE_MAX unused
IMPORTANCE_HIGH shows everywhere, makes noise and peeks
IMPORTANCE_DEFAULT shows everywhere, makes noise, but does not visually intrude
IMPORTANCE_LOW shows everywhere, but is not intrusive
IMPORTANCE_MIN only shows in the shade, below the fold
IMPORTANCE_NONE a notification with no importance; does not show in the shade
Notification channels enable us app developers to group our notifications into groups—channels—with the user
having the ability to modify notification settings for the entire channel at once.In Android O this feature is
introduced.Right now it is available developers preview.
Section 186.1: Notification Channel
What Are Notification Channels?
Notification channels enable us app developers to group our notifications into groups—channels—with the user
having the ability to modify notification settings for the entire channel at once. For example, for each channel, users
can completely block all notifications, override the importance level, or allow a notification badge to be shown. This
new feature helps in greatly improving the user experience of an app.
Create Notification Channels
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.ContextWrapper;
import android.graphics.Color;
public class NotificationUtils extends ContextWrapper {
private NotificationManager mManager;
public static final String ANDROID_CHANNEL_ID = "com.sai.ANDROID";
public static final String IOS_CHANNEL_ID = "com.sai.IOS";
public static final String ANDROID_CHANNEL_NAME = "ANDROID CHANNEL";
public static final String IOS_CHANNEL_NAME = "IOS CHANNEL";
public NotificationUtils(Context base) {
super(base);
createChannels();
}
public void createChannels() {
// create android channel
NotificationChannel androidChannel = new NotificationChannel(ANDROID_CHANNEL_ID,
ANDROID_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
// Sets whether notifications posted to this channel should display notification lights
androidChannel.enableLights(true);
// Sets whether notification posted to this channel should vibrate.
GoalKicker.com – Android™ Notes for Professionals 926
androidChannel.enableVibration(true);
// Sets the notification light color for notifications posted to this channel
androidChannel.setLightColor(Color.BLUE);
// Sets whether notifications posted to this channel appear on the lockscreen or not
androidChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
getManager().createNotificationChannel(androidChannel);
// create ios channel
NotificationChannel iosChannel = new NotificationChannel(IOS_CHANNEL_ID,
IOS_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
iosChannel.enableLights(true);
iosChannel.enableVibration(true);
iosChannel.setLightColor(Color.GRAY);
iosChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
getManager().createNotificationChannel(iosChannel);
}
private NotificationManager getManager() {
if (mManager == null) {
mManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
}
return mManager;
}}
In the code above, we created two instances of the NotificationChannel, passing uniqueid a channel name, and also
an importance level in its constructor. For each notification channel, we applied following characteristics.
1. Sound
2. Lights
3. Vibration
4. Notification to show on lock screen.
Finally, we got the NotificationManager from the system and then registered the channel by calling the method
createNotificationChannel(), passing the channel we have created.
We can create multiple notification channels all at once with createNotificationChannels(), passing a Java list of
NotificationChannel instances. You can get all notification channels for an app with getNotificationChannels() and
get a specific channel with getNotificationChannel(), passing only the channel id as an argument.
Importance Level in Notification Channels
Method Description
IMPORTANCE_MAX unused
IMPORTANCE_HIGH shows everywhere, makes noise and peeks
IMPORTANCE_DEFAULT shows everywhere, makes noise, but does not visually intrude
IMPORTANCE_LOW shows everywhere, but is not intrusive,value is 0
IMPORTANCE_MIN only shows in the shade, below the fold
IMPORTANCE_NONE a notification with no importance; does not show in the shade
Create Notification and Post to channel
We have created two notification one using NotificationUtils another using NotificationBuilder.
GoalKicker.com – Android™ Notes for Professionals 927
public Notification.Builder getAndroidChannelNotification(String title, String body) {
return new Notification.Builder(getApplicationContext(), ANDROID_CHANNEL_ID)
.setContentTitle(title)
.setContentText(body)
.setSmallIcon(android.R.drawable.stat_notify_more)
.setAutoCancel(true);
}
public Notification.Builder getIosChannelNotification(String title, String body) {
return new Notification.Builder(getApplicationContext(), IOS_CHANNEL_ID)
.setContentTitle(title)
.setContentText(body)
.setSmallIcon(android.R.drawable.stat_notify_more)
.setAutoCancel(true);
}
We can also set NotificationChannel Using Notification.Builder().For that we can use setChannel(String channelId).
Update Notification Channel Settings
Once you create a notification channel, the user is in charge of its settings and behavior. You can call
createNotificationChannel() again to rename an existing notification channel, or update its description. The
following sample code describes how you can redirect a user to the settings for a notification channel by creating
an intent to start an activity. In this case, the intent requires extended data including the ID of the notification
channel and the package name of your app.
@Override
protected void onCreate(Bundle savedInstanceState) {
//...
Button buttonAndroidNotifSettings = (Button) findViewById(R.id.btn_android_notif_settings);
buttonAndroidNotifSettings.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
i.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
i.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationUtils.ANDROID_CHANNEL_ID);
startActivity(i);
}
});
}
XML file:
<!--...-->
<Button
android:id="@+id/btn_android_notif_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Notification Settings"/>
<!--...-->
Deleting Notification Channel
You can delete notification channels by calling deleteNotificationChannel().
NotificationManager mNotificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
GoalKicker.com – Android™ Notes for Professionals 928
// The id of the channel.
String id = "my_channel_01";
NotificationChannel mChannel = mNotificationManager.getNotificationChannel(id);
mNotificationManager.deleteNotificationChannel(mChannel);
Now Create MainActivity and xml
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_margin="16dp"
tools:context="com.chikeandroid.tutsplusalerts.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tuts+ Android Channel"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.AppCompat.Title"/>
<EditText
android:id="@+id/et_android_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Title"/>
<EditText
android:id="@+id/et_android_author"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Author"/>
<Button
android:id="@+id/btn_send_android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
GoalKicker.com – Android™ Notes for Professionals 929
android:text="Tuts+ IOS Channel"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.AppCompat.Title"/>
<EditText
android:id="@+id/et_ios_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Title"
/>
<EditText
android:id="@+id/et_ios_author"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Author"/>
<Button
android:id="@+id/btn_send_ios"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>
</LinearLayout>
MainActivity.java
we are going to edit our MainActivity so that we can get the title and author from the EditText components and
then send these to the Android channel. We get the Notification.Builder for the Android channel we created in our
NotificationUtils, and then notify the NotificationManager.
import android.app.Notification; import android.os.Bundle; import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils; import android.view.View; import android.widget.Button; import
android.widget.EditText;
public class MainActivity extends AppCompatActivity {
private NotificationUtils mNotificationUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mNotificationUtils = new NotificationUtils(this);
final EditText editTextTitleAndroid = (EditText) findViewById(R.id.et_android_title);
final EditText editTextAuthorAndroid = (EditText) findViewById(R.id.et_android_author);
Button buttonAndroid = (Button) findViewById(R.id.btn_send_android);
buttonAndroid.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String title = editTextTitleAndroid.getText().toString();
String author = editTextAuthorAndroid.getText().toString();
if(!TextUtils.isEmpty(title) && !TextUtils.isEmpty(author)) {
Notification.Builder nb = mNotificationUtils.
getAndroidChannelNotification(title, "By " + author);
GoalKicker.com – Android™ Notes for Professionals 930
mNotificationUtils.getManager().notify(107, nb.build());
}
}
});
}
}
GoalKicker.com – Android™ Notes for Professionals 931
Chapter 187: Robolectric
Unit testing is taking a piece of code and testing it independently without any other dependencies or parts of the
system running (for example the database).
Robolectric is a unit test framework that de-fangs the Android SDK jar so you can test-drive the development of
your Android app. Tests run inside the JVM on your workstation in seconds.
Combing them both allows you to run fast tests on the JVN still using the Android API's.
Section 187.1: Robolectric test
@RunWith(RobolectricTestRunner.class)
public class MyActivityTest {
@Test
public void clickingButton_shouldChangeResultsViewText() throws Exception {
MyActivity activity = Robolectric.setupActivity(MyActivity.class);
Button button = (Button) activity.findViewById(R.id.button);
TextView results = (TextView) activity.findViewById(R.id.results);
button.performClick();
assertThat(results.getText().toString()).isEqualTo("Robolectric Rocks!");
}
}
Section 187.2: Configuration
To configure robolectric add @Config annotation to test class or method.
Run with custom Application class
@RunWith(RobolectricTestRunner.class)
@Config(application = MyApplication.class)
public final class MyTest {
}
Set target SDK
@RunWith(RobolectricTestRunner.class)
@Config(sdk = Build.VERSION_CODES.LOLLIPOP)
public final class MyTest {
}
Run with custom manifest
When specified, robolectric will look relative to the current directory. Default value is AndroidManifest.xml
Resources and assets will be loaded relative to the manifest.
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "path/AndroidManifest.xml")
public final class MyTest {
}
GoalKicker.com – Android™ Notes for Professionals 932
Use qualifiers
Possible qualifiers can be found in android docs.
@RunWith(RobolectricTestRunner.class)
public final class MyTest {
@Config(qualifiers = "sw600dp")
public void testForTablet() {
}
}
GoalKicker.com – Android™ Notes for Professionals 933
Chapter 188: Moshi
Moshi is a modern JSON library for Android and Java. It makes it easy to parse JSON into Java objects and Java back
into JSON.
Section 188.1: JSON into Java
String json = ...;
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<BlackjackHand> jsonAdapter = moshi.adapter(BlackjackHand.class);
BlackjackHand blackjackHand = jsonAdapter.fromJson(json);
System.out.println(blackjackHand);
Section 188.2: serialize Java objects as JSON
BlackjackHand blackjackHand = new BlackjackHand(
new Card('6', SPADES),
Arrays.asList(new Card('4', CLUBS), new Card('A', HEARTS)));
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<BlackjackHand> jsonAdapter = moshi.adapter(BlackjackHand.class);
String json = jsonAdapter.toJson(blackjackHand);
System.out.println(json);
Section 188.3: Built in Type Adapters
Moshi has built-in support for reading and writing Java’s core data types:
Primitives (int, float, char...) and their boxed counterparts (Integer, Float, Character...).
Arrays
Collections
Lists
Sets
Maps Strings Enums
It supports your model classes by writing them out field-by-field. In the example above Moshi uses these classes:
class BlackjackHand {
public final Card hidden_card;
public final List<Card> visible_cards;
...
}
class Card {
public final char rank;
public final Suit suit;
...
}
enum Suit {
CLUBS, DIAMONDS, HEARTS, SPADES;
}
to read and write this JSON:
GoalKicker.com – Android™ Notes for Professionals 934
{
"hidden_card": {
"rank": "6",
"suit": "SPADES"
},
"visible_cards": [
{
"rank": "4",
"suit": "CLUBS"
},
{
"rank": "A",
"suit": "HEARTS"
}
]
}
GoalKicker.com – Android™ Notes for Professionals 935
Chapter 189: Strict Mode Policy : A tool to
catch the bug in the Compile Time.
Strict Mode is a special class introduced in Android 2.3 for debugging. This developer tools detect things done
accidentally and bring them to our attention so that we can fix them. It is most commonly used to catch the
accidental disk or network access on the applications’ main thread, where UI operations are received and
animations takes place. StrictMode is basically a tool to catch the bug in the Compile Time mode.
Section 189.1: The below Code Snippet is to setup the
StrictMode for Thread Policies. This Code is to be set at the
entry points to our application
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskWrites()
.penaltyLog() //Logs a message to LogCat
.build())
Section 189.2: The below code deals with leaks of memory,
like it detects when in SQLLite finalize is called or not
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectLeakedClosableObjects()
.penaltyLog()
.build());
GoalKicker.com – Android™ Notes for Professionals 936
Chapter 190: Internationalization and
localization (I18N and L10N)
Internationalization (i18n) and Localization (L10n) are used to adapt software according to differences in languages,
regional differences and target audience.
Internationalization : the process of planning for future localization i.e. making the software design flexible to an
extent that it can adjust and adapt to future localization efforts.
Localization : the process of adapting the software to a particular region/country/market (locale).
Section 190.1: Planning for localization : enable RTL support in
Manifest
RTL (Right-to-left) support is an essential part in planning for i18n and L10n. Unlike English language which is
written from left to right, many languages like Arabic, Japanese, Hebrew, etc. are written from right to left. To
appeal to a more global audience, it is a good idea to plan your layouts to support these language from the very
beginning of the project, so that adding localization is easier later on.
RTL support can be enabled in an Android app by adding the supportsRtl tag in the AndroidManifest, like so :
<application
...
android:supportsRtl="true"
...>
...
</application>
Section 190.2: Planning for localization : Add RTL support in
Layouts
Starting SDK 17 (Android 4.2), RTL support was added in Android layouts and is an essential part of localization.
Going forward, the left/right notation in layouts should be replaced by start/end notation. If, however, your
project has a minSdk value less than 17, then both left/right and start/end notation should be used in layouts.
For relative layouts, alignParentStart and alignParentEnd should be used, like so:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"/>
</RelativeLayout>
GoalKicker.com – Android™ Notes for Professionals 937
For specifying gravity and layout gravity, similar notation should be used, like so :
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left|start"
android:gravity="left|start"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|end"
android:gravity="right|end"/>
Paddings and margins should also be specified accordingly, like so :
<include layout="@layout/notification"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:paddingLeft="128dp"
android:paddingStart="128dp"
android:layout_toLeftOf="@id/cancel_action"
android:layout_toStartOf="@id/cancel_action"/>
<include layout="@layout/notification2"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:paddingRight="128dp"
android:paddingEnd="128dp"
android:layout_toRightOf="@id/cancel_action"
android:layout_toEndOf="@id/cancel_action"/>
Section 190.3: Planning for localization : Test layouts for RTL
To test if the layouts that have been created are RTL compatible, do the following :
Go to Settings -> Developer options -> Drawing -> Force RTL layout direction
Enabling this option would force the device to use RTL locales and you can easily verify all parts of the app for RTL
support. Note that you don't need to actually add any new locales/ language support up till this point.
Section 190.4: Coding for Localization : Creating default
strings and resources
The first step for coding for localization is to create default resources. This step is so implicit that many developers
do not even think about it. However, creating default resources is important because if the device runs on an
unsupported locale, it would load all of its resources from the default folders. If even one of the resources is
missing from the default folders, the app would simply crash.
The default set of strings should be put in the following folder at the specified location:
res/values/strings.xml
GoalKicker.com – Android™ Notes for Professionals 938
This file should contain the strings in the language that majority users of the app are expected to speak.
Also, default resources for the app should be placed at the following folders and locations :
res/drawable/
res/layout/
If your app requires folders like anim, or xml, the default resources should be added to the following folders and
locations:
res/anim/
res/xml/
res/raw/
Section 190.5: Coding for localization : Providing alternative
strings
To provide translations in other languages (locales), we need to create a strings.xml in a separate folder by the
following convention :
res/values-<locale>/strings.xml
An example for the same is given below:
In this example, we have default English strings in the file res/values/strings.xml, French translations are
provided in the folder res/values-fr/strings.xml and Japanese translations are provided in the folder
res/values-ja/strings.xml
Other translations for other locales can similarly be added to the app.
A complete list of locale codes can be found here : ISO 639 codes
Non-translatable Strings:
Your project may have certain strings that are not to be translated. Strings which are used as keys for
SharedPreferences or strings which are used as symbols, fall in this category. These strings should be stored only in
the default strings.xml and should be marked with a translatable="false" attribute. e.g.
<string name="pref_widget_display_label_hot">Hot News</string>
<string name="pref_widget_display_key" translatable="false">widget_display</string>
<string name="pref_widget_display_hot" translatable="false">0</string>
GoalKicker.com – Android™ Notes for Professionals 939
This attribute is important because translations are often carried out by professionals who are bilingual. This would
allow these persons involved in translations to identify strings which are not to be translated, thus saving time and
money.
Section 190.6: Coding for localization : Providing alternate
layouts
Creating language specific layouts is often unnecessary if you have specified the correct start/end notation, as
described in the earlier example. However, there may be situations where the defaults layouts may not work
correctly for certain languages. Sometimes, left-to-right layouts may not translate for RTL languages. It is necessary
to provide the correct layouts in such cases.
To provide complete optimization for RTL layouts, we can use entirely separate layout files using the ldrtl resource
qualifier (ldrtl stands for layout-direction-right-to-left}). For example, we can save your default layout files in
res/layout/ and our RTL optimized layouts in res/layout-ldrtl/.
The ldrtl qualifier is great for drawable resources, so that you can provide graphics that are oriented in the
direction corresponding to the reading direction.
Here is a great post which describes the precedence of the ldrtl layouts : Language specific layouts
GoalKicker.com – Android™ Notes for Professionals 940
Chapter 191: Fast way to setup
Retrolambda on an android project.
Retrolambda is a library which allows to use Java 8 lambda expressions, method references and try-with-resources
statements on Java 7, 6 or 5.
The Gradle Retrolambda Plug-in allows to integrate Retrolambda into a Gradle based build. This allows for example
to use these constructs in an Android application, as standard Android development currently does not yet support
Java 8.
Section 191.1: Setup and example how to use:
Setup Steps:
1. Download and install jdk8.
2. Add the following to your project’s main build.gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'me.tatarka:gradle-retrolambda:3.2.3'
}
}
3. Now add this to your application module’s build.gradle
apply plugin: 'com.android.application' // or apply plugin: 'java'
apply plugin: 'me.tatarka.retrolambda'
4. Add these lines to your application module’s build.gradle to inform the IDE of the language level:
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
Example:
So things like this:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
log("Clicked");
}
});
Become this:
GoalKicker.com – Android™ Notes for Professionals 941
button.setOnClickListener(v -> log("Clicked"));
GoalKicker.com – Android™ Notes for Professionals 942
Chapter 192: How to use SparseArray
A SparseArray is an alternative for a Map. A Map requires its keys to be objects. The phenomenon of autoboxing
occurs when we want to use a primitive int value as key. The compiler automatically converts primitive values to
their boxed types (e.g. int to Integer). The difference in memory footprint is noticeable: int uses 4 bytes, Integer
uses 16 bytes. A SparseArray uses int as key value.
Section 192.1: Basic example using SparseArray
class Person {
String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return name != null ? name.equals(person.name) : person.name == null;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
final Person steve = new Person("Steve");
Person[] persons = new Person[] { new Person("John"), new Person("Gwen"), steve, new Person("Rob")
};
int[] identifiers = new int[] {1234, 2345, 3456, 4567};
final SparseArray<Person> demo = new SparseArray<>();
// Mapping persons to identifiers.
for (int i = 0; i < persons.length; i++) {
demo.put(identifiers[i], persons[i]);
}
// Find the person with identifier 1234.
Person id1234 = demo.get(1234); // Returns John.
// Find the person with identifier 6410.
Person id6410 = demo.get(6410); // Returns null.
// Find the 3rd person.
Person third = demo.valueAt(3); // Returns Rob.
GoalKicker.com – Android™ Notes for Professionals 943
// Find the 42th person.
//Person fortysecond = demo.valueAt(42); // Throws ArrayIndexOutOfBoundsException.
// Remove the last person.
demo.removeAt(demo.size() - 1); // Rob removed.
// Remove the person with identifier 1234.
demo.delete(1234); // John removed.
// Find the index of Steve.
int indexOfSteve = demo.indexOfValue(steve);
// Find the identifier of Steve.
int identifierOfSteve = demo.keyAt(indexOfSteve);
Tutorial on YouTube
GoalKicker.com – Android™ Notes for Professionals 944
Chapter 193: Shared Element Transitions
Here you find examples for transition between Activities or Fragments using a shared element. An example for
this behaviour is the Google Play Store App which translates an App's icon from the list to the App's details view.
Section 193.1: Shared Element Transition between two
Fragments
In this example, one of two different ImageViews should be translated from the ChooserFragment to the
DetailFragment.
In the ChooserFragment layout we need the unique transitionName attributes:
<ImageView
android:id="@+id/image_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_first"
android:transitionName="fistImage" />
<ImageView
android:id="@+id/image_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_second"
android:transitionName="secondImage" />
In the ChooserFragments class, we need to pass the View which was clicked and an ID to the parent Activity wich
is handling the replacement of the fragments (we need the ID to know which image resource to show in the
DetailFragment). How to pass information to a parent activity in detail is surely covered in another documentation.
view.findViewById(R.id.image_first).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mCallback != null) {
mCallback.showDetailFragment(view, 1);
}
}
});
view.findViewById(R.id.image_second).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mCallback != null) {
mCallback.showDetailFragment(view, 2);
}
}
});
In the DetailFragment, the ImageView of the shared element also needs the unique transitionName attribute.
<ImageView
android:id="@+id/image_shared"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
GoalKicker.com – Android™ Notes for Professionals 945
android:transitionName="sharedImage" />
In the onCreateView() method of the DetailFragment, we have to decide which image resource should be shown
(if we don't do that, the shared element will disappear after the transition).
public static DetailFragment newInstance(Bundle args) {
DetailFragment fragment = new DetailFragment();
fragment.setArguments(args);
return fragment;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_detail, container, false);
ImageView sharedImage = (ImageView) view.findViewById(R.id.image_shared);
// Check which resource should be shown.
int type = getArguments().getInt("type");
// Show image based on the type.
switch (type) {
case 1:
sharedImage.setBackgroundResource(R.drawable.ic_first);
break;
case 2:
sharedImage.setBackgroundResource(R.drawable.ic_second);
break;
}
return view;
}
The parent Activity is receiving the callbacks and handles the replacement of the fragments.
@Override
public void showDetailFragment(View sharedElement, int type) {
// Get the chooser fragment, which is shown in the moment.
Fragment chooserFragment = getFragmentManager().findFragmentById(R.id.fragment_container);
// Set up the DetailFragment and put the type as argument.
Bundle args = new Bundle();
args.putInt("type", type);
Fragment fragment = DetailFragment.newInstance(args);
// Set up the transaction.
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Define the shared element transition.
fragment.setSharedElementEnterTransition(new DetailsTransition());
fragment.setSharedElementReturnTransition(new DetailsTransition());
// The rest of the views are just fading in/out.
fragment.setEnterTransition(new Fade());
chooserFragment.setExitTransition(new Fade());
// Now use the image's view and the target transitionName to define the shared element.
transaction.addSharedElement(sharedElement, "sharedImage");
GoalKicker.com – Android™ Notes for Professionals 946
// Replace the fragment.
transaction.replace(R.id.fragment_container, fragment, fragment.getClass().getSimpleName());
// Enable back navigation with shared element transitions.
transaction.addToBackStack(fragment.getClass().getSimpleName());
// Finally press play.
transaction.commit();
}
Not to forget - the Transition itself. This example moves and scales the shared element.
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class DetailsTransition extends TransitionSet {
public DetailsTransition() {
setOrdering(ORDERING_TOGETHER);
addTransition(new ChangeBounds()).
addTransition(new ChangeTransform()).
addTransition(new ChangeImageTransform());
}
}
GoalKicker.com – Android™ Notes for Professionals 947
Chapter 194: Android Things
Section 194.1: Controlling a Servo Motor
This example assumes you have a servo with the following characteristics, which happen to be typical:
movement between 0 and 180 degrees
pulse period of 20 ms
minimum pulse length of 0.5 ms
maximum pulse length of 2.5 ms
You need to check if those values match your hardware, since forcing it to go outside its specified operating range
can damage the servo. A damaged servo in turn has the potential to damage your Android Things device. The
example ServoController class consists of two methods, setup() and setPosition():
public class ServoController {
private double periodMs, maxTimeMs, minTimeMs;
private Pwm pin;
public void setup(String pinName) throws IOException {
periodMs = 20;
maxTimeMs = 2.5;
minTimeMs = 0.5;
PeripheralManagerService service = new PeripheralManagerService();
pin = service.openPwm(pinName);
pin.setPwmFrequencyHz(1000.0d / periodMs);
setPosition(90);
pin.setEnabled(true);
}
public void setPosition(double degrees) {
double pulseLengthMs = (degrees / 180.0 * (maxTimeMs - minTimeMs)) + minTimeMs;
if (pulseLengthMs < minTimeMs) {
pulseLengthMs = minTimeMs;
} else if (pulseLengthMs > maxTimeMs) {
pulseLengthMs = maxTimeMs;
}
double dutyCycle = pulseLengthMs / periodMs * 100.0;
Log.i(TAG, "Duty cycle = " + dutyCycle + " pulse length = " + pulseLengthMs);
try {
pin.setPwmDutyCycle(dutyCycle);
} catch (IOException e) {
e.printStackTrace();
}
}
}
You can discover pin names that support PWM on your device as follows:
PeripheralManagerService service = new PeripheralManagerService();
for (String pinName : service.getPwmList() ) {
GoalKicker.com – Android™ Notes for Professionals 948
Log.i("ServoControlled","Pwm pin found: " + pinName);
}
In order to make your servo swinging forever between 80 degrees and 100 degrees, you can simply use the
following code:
final ServoController servoController = new ServoController(pinName);
Thread th = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
servoController.setPosition(80);
Thread.sleep(500);
servoController.setPosition(100);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
th.start();
You can compile and deploy all of the above code without actually hooking any servo motors to the computing
device. For the wiring, refer to your computing device pinout chart (e.g. a Raspberry Pi 3 pinout chart is available
here).
Then you need to hook your servo to Vcc, Gnd, and signal.
GoalKicker.com – Android™ Notes for Professionals 949
Chapter 195: Library Dagger 2:
Dependency Injection in Applications
Dagger 2, as explained on GitHub, is a compile-time evolution approach to dependency injection. Taking the
approach started in Dagger 1.x to its ultimate conclusion, Dagger 2.x eliminates all reflection, and improves code
clarity by removing the traditional ObjectGraph/Injector in favor of user-specified @Component interfaces.
Section 195.1: Create @Module Class and @Singleton
annotation for Object
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class VehicleModule {
@Provides @Singleton
Motor provideMotor(){
return new Motor();
}
@Provides @Singleton
Vehicle provideVehicle(){
return new Vehicle(new Motor());
}
}
Every provider (or method) must have the @Provides annotation and the class must have the @Module annotation.
The @Singleton annotation indicates that there will be only one instance of the object.
Section 195.2: Request Dependencies in Dependent Objects
Now that you have the providers for your different models, you need to request them. Just as Vehicle needs Motor,
you have to add the @Inject annotation in the Vehicle constructor as follows:
@Inject
public Vehicle(Motor motor){
this.motor = motor;
}
You can use the @Inject annotation to request dependencies in the constructor, fields, or methods. In this
example, I'm keeping the injection in the constructor.
Section 195.3: Connecting @Modules with @Inject
The connection between the provider of dependencies, @Module, and the classes requesting them through @Inject
is made using @Component, which is an interface:
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {VehicleModule.class})
GoalKicker.com – Android™ Notes for Professionals 950
public interface VehicleComponent {
Vehicle provideVehicle();
}
For the @Component annotation, you have to specify which modules are going to be used. In this example
VehicleModule is used, which is defined in this example. If you need to use more modules, then just add them
using a comma as a separator.
Section 195.4: Using @Component Interface to Obtain Objects
Now that you have every connection ready, you have to obtain an instance of this interface and invoke its methods
to obtain the object you need:
VehicleComponent component = Dagger_VehicleComponent.builder().vehicleModule(new
VehicleModule()).build();
vehicle = component.provideVehicle();
Toast.makeText(this, String.valueOf(vehicle.getSpeed()), Toast.LENGTH_SHORT).show();
When you try to create a new object of the interface with the @Component annotation, you have to do it using the
prefix Dagger_<NameOfTheComponentInterface>, in this case Dagger_VehicleComponent, and then use the builder
method to call every module within.
GoalKicker.com – Android™ Notes for Professionals 951
Chapter 196: JCodec
Section 196.1: Getting Started
You can get JCodec automatically with maven. For this just add below snippet to your pom.xml .
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec-javase</artifactId>
<version>0.1.9</version>
</dependency>
Section 196.2: Getting frame from movie
Getting a single frame from a movie ( supports only AVC, H.264 in MP4, ISO BMF, Quicktime container ):
int frameNumber = 150;
BufferedImage frame = FrameGrab.getFrame(new File("filename.mp4"), frameNumber);
ImageIO.write(frame, "png", new File("frame_150.png"));
Getting a sequence of frames from a movie ( supports only AVC, H.264 in MP4, ISO BMF, Quicktime container ):
double startSec = 51.632;
FileChannelWrapper ch = null;
try {
ch = NIOUtils.readableFileChannel(new File("filename.mp4"));
FrameGrab fg = new FrameGrab(ch);
grab.seek(startSec);
for (int i = 0; i < 100; i++) {
ImageIO.write(grab.getFrame(), "png",
new File(System.getProperty("user.home"), String.format("Desktop/frame_%08d.png", i)));
}
} finally {
NIOUtils.closeQuietly(ch);
}
GoalKicker.com – Android™ Notes for Professionals 952
Chapter 197: Formatting phone numbers
with pattern.
This example show you how to format phone numbers with a patter
You will need the following library in your gradle.
compile 'com.googlecode.libphonenumber:libphonenumber:7.2.2'
Section 197.1: Patterns + 1 (786) 1234 5678
Given a normalized phone number like +178612345678 we will get a formatted number with the provided pattern.
private String getFormattedNumber(String phoneNumber) {
PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
Phonemetadata.NumberFormat numberFormat = new Phonemetadata.NumberFormat();
numberFormat.pattern = "(\\d{3})(\\d{3})(\\d{4})";
numberFormat.format = "($1) $2-$3";
List<Phonemetadata.NumberFormat> newNumberFormats = new ArrayList<>();
newNumberFormats.add(numberFormat);
Phonenumber.PhoneNumber phoneNumberPN = null;
try {
phoneNumberPN = phoneNumberUtil.parse(phoneNumber, Locale.US.getCountry());
phoneNumber = phoneNumberUtil.formatByPattern(phoneNumberPN,
PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL, newNumberFormats);
} catch (NumberParseException e) {
e.printStackTrace();
}
return phoneNumber;
}
GoalKicker.com – Android™ Notes for Professionals 953
Chapter 198: Paint
A paint is one of the four objects needed to draw, along with a Canvas (holds drawing calls), a Bitmap (holds the
pixels), and a drawing primitive (Rect, Path, Bitmap...)
Section 198.1: Creating a Paint
You can create a new paint with one of these 3 constructors:
new Paint() Create with default settings
new Paint(int flags) Create with flags
new Paint(Paint from) Copy settings from another paint
It is generally suggested to never create a paint object, or any other object in onDraw() as it can lead to
performance issues. (Android Studio will probably warn you) Instead, make it global and initialize it in your class
constructor like so:
public class CustomView extends View {
private Paint paint;
public CustomView(Context context) {
super(context);
paint = new Paint();
//...
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(0xFF000000);
// ...
}
}
Section 198.2: Setting up Paint for text
Text drawing settings
setTypeface(Typeface typeface) Set the font face. See Typeface
setTextSize(int size) Set the font size, in pixels.
setColor(int color) Set the paint drawing color, including the text color. You can also use setARGB(int a,
int r, int g, int b and setAlpha(int alpha)
setLetterSpacing(float size) Set the spacing between characters, in ems. Default value is 0, a negative
value will tighten the text, while a positive one will expand it.
setTextAlign(Paint.Align align) Set text alignment relative to its origin. Paint.Align.LEFT will draw it to
the right of the origin, RIGHT will draw it to the left, and CENTER will draw it centered on the origin
(horizontally)
setTextSkewX(float skewX) This could be considered as fake italic. SkewX represents the horizontal offset
of the text bottom. (use -0.25 for italic)
setStyle(Paint.Style style) Fill text FILL, Stroke text STROKE, or both FILL_AND_STROKE
Note that you can use TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, size,
getResources().getDisplayMetrics()) to convert from SP or DP to pixels.
GoalKicker.com – Android™ Notes for Professionals 954
Measuring text
float width = paint.measureText(String text) Measure the width of text
float height = paint.ascent() Measure the height of text
paint.getTextBounds(String text, int start, int end, Rect bounds Stores the text dimensions. You
have allocate the Rect, it cannot be null:
String text = "Hello world!";
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
There are other methods for measuring, however these three should fit most purposes.
Section 198.3: Setting up Paint for drawing shapes
setStyle(Paint.Style style) Filled shape FILL, Stroke shape STROKE, or both FILL_AND_STROKE
setColor(int color) Set the paint drawing color. You can also use setARGB(int a, int r, int g, int b
and setAlpha(int alpha)
setStrokeCap(Paint.Cap cap) Set line caps, either ROUND, SQUARE, or BUTT (none) See this.
setStrokeJoin(Paint.Join join) Set line joins, either MITER (pointy), ROUND, or BEVEL. See this.
setStrokeMiter(float miter) Set miter join limit. This can prevent miter join from going on indefinitively,
turning it into a bevel join after x pixels. See this.
setStrokeWidth(float width) Set stroke width. 0 will draw in hairline mode, independant of the canvas
matrix. (always 1 pixel)
Section 198.4: Setting flags
You can set the following flags in the constructor, or with setFlags(int flags)
Paint.ANTI_ALIAS_FLAG Enable antialiasing, smooths the drawing.
Paint.DITHER_FLAG Enable dithering. If color precision is higher than the device's, this will happen.
Paint.EMBEDDED_BITMAP_TEXT_FLAG Enables the use of bitmap fonts.
Paint.FAKE_BOLD_TEXT_FLAG will draw text with a fake bold effect, can be used instead of using a bold
typeface. Some fonts have styled bold, fake bold won't
Paint.FILTER_BITMAP_FLAG Affects the sampling of bitmaps when transformed.
Paint.HINTING_OFF, Paint.HINTING_ON Toggles font hinting, see this
Paint.LINEAR_TEXT_FLAG Disables font scaling, draw operations are scaled instead
Paint.SUBPIXEL_TEXT_FLAG Text will be computed using subpixel accuracy.
Paint.STRIKE_THRU_TEXT_FLAG Text drawn will be striked
Paint.UNDERLINE_TEXT_FLAG Text drawn will be underlined
You can add a flag and remove flags like this:
Paint paint = new Paint();
paint.setFlags(paint.getFlags() | Paint.FLAG); // Add flag
paint.setFlags(paint.getFlags() & ~Paint.FLAG); // Remove flag
Trying to remove a flag that isn't there or adding a flag that is already there won't change anything. Also note that
most flags can also be set using set<Flag>(boolean enabled), for example setAntialias(true).
You can use paint.reset() to reset the paint to its default settings. The only default flag is
EMBEDDED_BITMAP_TEXT_FLAG. It will be set even if you use new Paint(0), you will have
GoalKicker.com – Android™ Notes for Professionals 955
Chapter 199: What is ProGuard? What is
use in Android?
Proguard is free Java class file shrinker, optimizer, obfuscator, and preverifier. It detects and removes unused
classes, fields, methods, and attributes. It optimizes bytecode and removes unused instructions. It renames the
remaining classes, fields, and methods using short meaningless names.
Section 199.1: Shrink your code and resources with proguard
To make your APK file as small as possible, you should enable shrinking to remove unused code and resources in
your release build. This page describes how to do that and how to specify what code and resources to keep or
discard during the build.
Code shrinking is available with ProGuard, which detects and removes unused classes, fields, methods, and
attributes from your packaged app, including those from included code libraries (making it a valuable tool for
working around the 64k reference limit). ProGuard also optimizes the bytecode, removes unused code instructions,
and obfuscates the remaining classes, fields, and methods with short names. The obfuscated code makes your APK
difficult to reverse engineer, which is especially valuable when your app uses security-sensitive features, such as
licensing verification.
Resource shrinking is available with the Android plugin for Gradle, which removes unused resources from your
packaged app, including unused resources in code libraries. It works in conjunction with code shrinking such that
once unused code has been removed, any resources no longer referenced can be safely removed as well.
Shrink Your Code
To enable code shrinking with ProGuard, add minifyEnabled true to the appropriate build type in your
build.gradle file.
Be aware that code shrinking slows down the build time, so you should avoid using it on your debug build if
possible. However, it's important that you do enable code shrinking on your final APK used for testing, because it
might introduce bugs if you do not sufficiently customize which code to keep.
For example, the following snippet from a build.gradle file enables code shrinking for the release build:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
...
}
In addition to the minifyEnabled property, the proguardFiles property defines the ProGuard rules:
The getDefaultProguardFile('proguard-android.txt') method gets the default ProGuard settings from the Android
SDK tools/proguard/ folder. Tip: For even more code shrinking, try the proguard-android-optimize.txt file
that's in the same location. It includes the same ProGuard rules, but with other optimizations that perform analysis
at the bytecode level—inside and across methods—to reduce your APK size further and help it run faster. The
proguard-rules.pro file is where you can add custom ProGuard rules. By default, this file is located at the root of
GoalKicker.com – Android™ Notes for Professionals 956
the module (next to the build.gradle file). To add more ProGuard rules that are specific to each build variant, add
another proguardFiles property in the corresponding productFlavor block. For example, the following Gradle file
adds flavor2-rules.pro to the flavor2 product flavor. Now flavor2 uses all three ProGuard rules because those from
the release block are also applied.
android {
...
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
productFlavors {
flavor1 {
}
flavor2 {
proguardFile 'flavor2-rules.pro'
}
}
}
GoalKicker.com – Android™ Notes for Professionals 957
Chapter 200: Create Android Custom
ROMs
Section 200.1: Making Your Machine Ready for Building!
Before you can build anything, you are required to make your machine ready for building. For this you need to
install a lot of libraries and modules. The most recommended Linux distribution is Ubuntu, so this example will
focus on installing everything that is needed on Ubuntu.
Installing Java
First, add the following Personal Package Archive (PPA): sudo apt-add-repository ppa:openjdk-r/ppa.
Then, update the sources by executing: sudo apt-get update.
Installing Additional Dependencies
All required additional dependencies can be installed by the following command:
sudo apt-get install git-core python gnupg flex bison gperf libsdl1.2-dev libesd0-dev libwxgtk2.8-
dev squashfs-tools build-essential zip curl libncurses5-dev zlib1g-dev openjdk-8-jre openjdk-8-jdk
pngcrush schedtool libxml2 libxml2-utils xsltproc lzop libc6-dev schedtool g++-multilib lib32z1-dev
lib32ncurses5-dev gcc-multilib liblz4-* pngquant ncurses-dev texinfo gcc gperf patch libtool
automake g++ gawk subversion expat libexpat1-dev python-all-dev binutils-static bc libcloog-isl-dev
libcap-dev autoconf libgmp-dev build-essential gcc-multilib g++-multilib pkg-config libmpc-dev
libmpfr-dev lzma* liblzma* w3m android-tools-adb maven ncftp figlet
Preparing the system for development
Now that all the dependencies are installed, let us prepare the system for development by executing:
sudo curl --create-dirs -L -o /etc/udev/rules.d/51-android.rules -O -L
https://raw.githubusercontent.com/snowdream/51-android/master/51-android.rules
sudo chmod 644 /etc/udev/rules.d/51-android.rules
sudo chown root /etc/udev/rules.d/51-android.rules
sudo service udev restart
adb kill-server
sudo killall adb
Finally, let us set up the cache and the repo by the following commands:
sudo install utils/repo /usr/bin/
sudo install utils/ccache /usr/bin/
Please note: We can also achieve this setup by running the automated scripts made by Akhil Narang (akhilnarang),
one of the maintainers of Resurrection Remix OS. These scripts can be found on GitHub.
GoalKicker.com – Android™ Notes for Professionals 958
Chapter 201: Genymotion for android
Genymotion is a fast third-party emulator that can be used instead of the default Android emulator. In some cases
it's as good as or better than developing on actual devices!
Section 201.1: Installing Genymotion, the free version
Step 1 - installing VirtualBox
Download and install VirtualBox according to your operating system. , it is required to run Genymotion.
Step 2 - downloading Genymotion
Go to the Genymotion download page and download Genymotion according to your operating system.
Note: you will need to create a new account OR log-in with your account.
Step 3 - Installing Genymotion
if on Linux then refer to this answer, to install and run a .bin file.
Step 4 - Installing Genymotion's emulators
run Genymotion
Press on the Add button (in top bar).
Log-In with your account and you will be able to browse the available emulators.
select and Install what you need.
Step 5 - Integrating genymotion with Android Studio
Genymotion, can be integrated with Android Studio via a plugin, here the steps to install it in Android Studio
go to File/Settings (for Windows and Linux) or to Android Studio/Preferences (for Mac OS X)
Select Plugins and click Browse Repositories.
Right-click on Genymotion and click Download and install.
You should now be able to see the plugin icon, see this image
Note, you might want to display the toolbar by clicking View > Toolbar.
Step 6 - Running Genymotion from Android Studio
go to File/Settings (for Windows and Linux) or to Android Studio/Preferences (for Mac OS X)
go to Other Settings/Genymotion and add the path of Genymotion's folder and apply your changes.
Now you should be able to run Genymotion's emulator by pressing the plugin icon and selecting an installed
emulator and then press start button!
GoalKicker.com – Android™ Notes for Professionals 959
Section 201.2: Google framework on Genymotion
If developers want to test Google Maps or any other Google service like Gmail,Youtube, Google drive etc. then they
first need to install Google framework on Genymotion. Here are the steps:
4.4 Kitkat
5.0 Lollipop
5.1 Lollipop
6.0 Marshmallow
7.0 Nougat
7.1 Nougat (webview patch)
1. Download from above link
2. Just drag & drop downloaded zip file to genymotion and restart
3. Add google account and download "Google Play Music" and Run.
Reference:
Stack overflow question on this topic
GoalKicker.com – Android™ Notes for Professionals 960
Chapter 202: ConstraintSet
This class allows you to define programmatically a set of constraints to be used with ConstraintLayout. It lets you
create and save constraints, and apply them to an existing ConstraintLayout.
Section 202.1: ConstraintSet with ContraintLayout
Programmatically
import android.content.Context;
import android.os.Bundle;
import android.support.constraint.ConstraintLayout;
import android.support.constraint.ConstraintSet;
import android.support.transition.TransitionManager;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
public class MainActivity extends AppCompatActivity {
ConstraintSet mConstraintSet1 = new ConstraintSet(); // create a Constraint Set
ConstraintSet mConstraintSet2 = new ConstraintSet(); // create a Constraint Set
ConstraintLayout mConstraintLayout; // cache the ConstraintLayout
boolean mOld = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context context = this;
mConstraintSet2.clone(context, R.layout.state2); // get constraints from layout
setContentView(R.layout.state1);
mConstraintLayout = (ConstraintLayout) findViewById(R.id.activity_main);
mConstraintSet1.clone(mConstraintLayout); // get constraints from ConstraintSet
}
public void foo(View view) {
TransitionManager.beginDelayedTransition(mConstraintLayout);
if (mOld = !mOld) {
mConstraintSet1.applyTo(mConstraintLayout); // set new constraints
} else {
mConstraintSet2.applyTo(mConstraintLayout); // set new constraints
}
}
}
GoalKicker.com – Android™ Notes for Professionals 961
Chapter 203: CleverTap
Quick hacks for the analytics and engagement SDK provided by CleverTap - Android
Section 203.1: Setting the debug level
In your custom application class, override the onCreate() method, add the line below:
CleverTapAPI.setDebugLevel(1);
Section 203.2: Get an instance of the SDK to record events
CleverTapAPI cleverTap;
try {
cleverTap = CleverTapAPI.getInstance(getApplicationContext());
} catch (CleverTapMetaDataNotFoundException e) {
// thrown if you haven't specified your CleverTap Account ID or Token in your AndroidManifest.xml
} catch (CleverTapPermissionsNotSatisfied e) {
// thrown if you haven’t requested the required permissions in your AndroidManifest.xml
}
GoalKicker.com – Android™ Notes for Professionals 962
Chapter 204: Publish a library to Maven
Repositories
Section 204.1: Publish .aar file to Maven
In order to publish to a repository in Maven format ,“maven-publish” plugin for gradle can be used.
The plugin should be added to build.gradle file in library module.
apply plugin: 'maven-publish'
You should define the publication and its identity attributes in build.gradle file too. This identity attributes will be
shown in the generated pom file and in future for importing this publication you will use them.You also need to
define which artifacts you want to publish,for example i just want to publish generated .aar file after building the
library.
publishing {
publications {
myPulication(MavenPublication) {
groupId 'com.example.project'
version '1.0.2'
artifactId 'myProject'
artifact("$buildDir/outputs/aar/myProject.aar")
}
}
}
You will also need to define your repository url
publishing{
repositories {
maven {
url "http://www.myrepository.com"
}
}
}
Here is full library build.gradle file
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
buildscript {
...
}
android {
...
}
publishing {
publications {
myPulication(MavenPublication) {
groupId 'com.example.project'
version '1.0.2'
artifactId 'myProject'
artifact("$buildDir/outputs/aar/myProject.aar")
}
GoalKicker.com – Android™ Notes for Professionals 963
}
repositories {
maven {
url "http://www.myrepository.com"
}
}
}
For publishing you can run gradle console command
gradle publish
or you can run from gradle tasks panel
GoalKicker.com – Android™ Notes for Professionals 964
Chapter 205: adb shell
Parameter Details
-e choose escape character, or "none"; default '~'
-n don't read from stdin
-T disable PTY allocation
-t force PTY allocation
-x disable remote exit codes and stdout/stderr separation
adb shell opens a Linux shell in a target device or emulator. It is the most powerful and versatile way to control an
Android device via adb.
This topic was split from ADB (Android Debug Bridge) due to reaching the limit of examples, many of which were
involving adb shell command.
Section 205.1: Granting & revoking API 23+ permissions
A one-liner that helps granting or revoking vulnerable permissions.
granting
adb shell pm grant <sample.package.id> android.permission.<PERMISSION_NAME>
revoking
adb shell pm revoke <sample.package.id> android.permission.<PERMISSION_NAME>
Granting all run-time permissions at a time on installation (-g)
adb install -g /path/to/sample_package.apk
Section 205.2: Send text, key pressed and touch events to
Android Device via ADB
execute the following command to insert the text into a view with a focus (if it supports text input)
Version ≥ 6.0
Send text on SDK 23+
adb shell "input keyboard text 'Paste text on Android Device'"
If already connected to your device via adb:
input text 'Paste text on Android Device'
Version < 6.0
Send text prior to SDK 23
adb shell "input keyboard text 'Paste%stext%son%sAndroid%sDevice'"
GoalKicker.com – Android™ Notes for Professionals 965
Spaces are not accepted as the input, replace them with %s.
Send events
To simulate pressing the hardware power key
adb shell input keyevent 26
or alternatively
adb shell input keyevent POWER
Even if you don't have a hardware key you still can use a keyevent to perform the equivalent action
adb shell input keyevent CAMERA
Send touch event as input
adb shell input tap Xpoint Ypoint
Send swipe event as input
adb shell input swipe Xpoint1 Ypoint1 Xpoint2 Ypoint2 [DURATION*]
*DURATION is optional, default=300ms. source
Get X and Y points by enabling pointer location in developer option.
ADB sample shell script
To run a script in Ubuntu, Create script.sh right click the file and add read/write permission and tick allow
executing file as program.
Open terminal emulator and run the command ./script.sh
Script.sh
for (( c=1; c<=5; c++ ))
do
adb shell input tap X Y
echo "Clicked $c times"
sleep 5s
done
For a comprehensive list of event numbers
shortlist of several interesting events ADB Shell Input Events
reference documentation
https://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_POWER.
GoalKicker.com – Android™ Notes for Professionals 966
Section 205.3: List packages
Prints all packages, optionally only those whose package name contains the text in <FILTER>.
adb shell pm list packages [options] <FILTER>
All <FILTER>
adb shell pm list packages
Attributes:
-f to see their associated file.
-i See the installer for the packages.
-u to also include uninstalled packages.
-u Also include uninstalled packages.
Attributes that filter:
-d for disabled packages.
-e for enabled packages.
-s for system packages.
-3 for third party packages.
--user <USER_ID> for a specific user space to query.
Section 205.4: Recording the display
Version ≥ 4.4
Recording the display of devices running Android 4.4 (API level 19) and higher:
adb shell screenrecord [options] <filename>
adb shell screenrecord /sdcard/demo.mp4
(press Ctrl-C to stop recording)
Download the file from the device:
adb pull /sdcard/demo.mp4
Note: Stop the screen recording by pressing Ctrl-C, otherwise the recording stops automatically at three
minutes or the time limit set by --time-limit.
adb shell screenrecord --size <WIDTHxHEIGHT>
Sets the video size: 1280x720. The default value is the device's native display resolution (if supported), 1280x720 if
not. For best results, use a size supported by your device's Advanced Video Coding (AVC) encoder.
GoalKicker.com – Android™ Notes for Professionals 967
adb shell screenrecord --bit-rate <RATE>
Sets the video bit rate for the video, in megabits per second. The default value is 4Mbps. You can increase the bit
rate to improve video quality, but doing so results in larger movie files. The following example sets the recording bit
rate to 5Mbps:
adb shell screenrecord --bit-rate 5000000 /sdcard/demo.mp4
adb shell screenrecord --time-limit <TIME>
Sets the maximum recording time, in seconds. The default and maximum value is 180 (3 minutes).
adb shell screenrecord --rotate
Rotates the output 90 degrees. This feature is experimental.
adb shell screenrecord --verbose
Displays log information on the command-line screen. If you do not set this option, the utility does not display any
information while running.
Note: This might not work on some devices.
Version < 4.4
The screen recording command isn't compatible with android versions pre 4.4
The screenrecord command is a shell utility for recording the display of devices running Android 4.4 (API
level 19) and higher. The utility records screen activity to an MPEG-4 file.
Section 205.5: Open Developer Options
adb shell am start -n com.android.settings/.DevelopmentSettings
Will navigate your device/emulator to the Developer Options section.
Section 205.6: Set Date/Time via adb
Version ≥ 6.0
Default SET format is MMDDhhmm[[CC]YY][.ss], that's (2 digits each)
For example, to set July 17'th 10:10am, without changing the current year, type:
adb shell 'date 07171010.00'
Tip 1: the date change will not be reflected immediately, and a noticable change will happen only after the system
clock advances to the next minute.
GoalKicker.com – Android™ Notes for Professionals 968
You can force an update by attaching a TIME_SET intent broadcast to your call, like that:
adb shell 'date 07171010.00 ; am broadcast -a android.intent.action.TIME_SET'
Tip 2: to synchronize Android's clock with your local machine:
Linux:
adb shell date `date +%m%d%H%M%G.%S`
Windows (PowerShell):
$currentDate = Get-Date -Format "MMddHHmmyyyy.ss" # Android's preferred format
adb shell "date $currentDate"
Both tips together:
adb shell 'date `date +%m%d%H%M%G.%S` ; am broadcast -a android.intent.action.TIME_SET'
Version < 6.0
Default SET format is 'YYYYMMDD.HHmmss'
adb shell 'date -s 20160117.095930'
Tip: to synchronize Android's clock with your local (linux based) machine:
adb shell date -s `date +%G%m%d.%H%M%S`
Section 205.7: Generating a "Boot Complete" broadcast
This is relevant for apps that implement a BootListener. Test your app by killing your app and then test with:
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -c android.intent.category.HOME -n
your.app/your.app.BootListener
(replace your.package/your.app.BootListener with proper values).
Section 205.8: Print application data
This command print all relevant application data:
version code
version name
granted permissions (Android API 23+)
etc..
adb shell dumpsys package <your.package.id>
Section 205.9: Changing file permissions using chmod
command
Notice, that in order to change file prmissions, your device need to be rooted, su binary doesn't come with factory
shipped devices!
GoalKicker.com – Android™ Notes for Professionals 969
Convention:
adb shell su -c "chmod <numeric-permisson> <file>"
Numeric permission constructed from user, group and world sections.
For example, if you want to change file to be readable, writable and executable by everyone, this will be your
command:
adb shell su -c "chmod 777 <file-path>"
Or
adb shell su -c "chmod 000 <file-path>"
if you intent to deny any permissions to it.
1st digit-specifies user permission, 2nd digit- specifies group permission, 3rd digit - specifies world (others)
permission.
Access permissions:
--- : binary value: 000, octal value: 0 (none)
--x : binary value: 001, octal value: 1 (execute)
-w- : binary value: 010, octal value: 2 (write)
-wx : binary value: 011, octal value: 3 (write, execute)
r-- : binary value: 100, octal value: 4 (read)
r-x : binary value: 101, octal value: 5 (read, execute)
rw- : binary value: 110, octal value: 6 (read, write)
rwx : binary value: 111, octal value: 7 (read, write, execute)
Section 205.10: View external/secondary storage content
View content:
adb shell ls \$EXTERNAL_STORAGE
adb shell ls \$SECONDARY_STORAGE
View path:
adb shell echo \$EXTERNAL_STORAGE
adb shell echo \$SECONDARY_STORAGE
Section 205.11: kill a process inside an Android device
Sometimes Android's logcat is running infinitely with errors coming from some process not own by you, draining
battery or just making it hard to debug your code.
A convenient way to fix the problem without restarting the device is to locate and kill the process causing the
problem.
From Logcat
03-10 11:41:40.010 1550-1627/? E/SomeProcess: ....
GoalKicker.com – Android™ Notes for Professionals 970
notice the process number: 1550
Now we can open a shell and kill the process. Note that we cannot kill root process.
adb shell
inside the shell we can check more about the process using
ps -x | grep 1550
and kill it if we want:
kill -9 1550
GoalKicker.com – Android™ Notes for Professionals 971
Chapter 206: Ping ICMP
The ICMP Ping request can be performed in Android by creating a new process to run the ping request. The
outcome of the request can be evaluated upon the completion of the ping request from within its process.
Section 206.1: Performs a single Ping
This example attempts a single Ping request. The ping command inside the runtime.exec method call can be
modified to any valid ping command you might perform yourself in the command line.
try {
Process ipProcess = runtime.exec("/system/bin/ping -c 1 8.8.8.8");
int exitValue = ipProcess.waitFor();
ipProcess.destroy();
if(exitValue == 0){
// Success
} else {
// Failure
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
GoalKicker.com – Android™ Notes for Professionals 972
Chapter 207: AIDL
AIDL is Android interface definition language.
What? Why? How ?
What? It is a bounded services. This AIDL service will be active till atleast one of the client is exist. It works based on
marshaling and unmarshaling concept.
Why? Remote applications can access your service + Multi Threading.(Remote application request).
How? Create the .aidl file Implement the interface Expose the interface to clients
Section 207.1: AIDL Service
ICalculator.aidl
// Declare any non-default types here with import statements
interface ICalculator {
int add(int x,int y);
int sub(int x,int y);
}
AidlService.java
public class AidlService extends Service {
private static final String TAG = "AIDLServiceLogs";
private static final String className = " AidlService";
public AidlService() {
Log.i(TAG, className+" Constructor");
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
Log.i(TAG, className+" onBind");
return iCalculator.asBinder();
}
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, className+" onCreate");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, className+" onDestroy");
}
GoalKicker.com – Android™ Notes for Professionals 973
ICalculator.Stub iCalculator = new ICalculator.Stub() {
@Override
public int add(int x, int y) throws RemoteException {
Log.i(TAG, className+" add Thread Name: "+Thread.currentThread().getName());
int z = x+y;
return z;
}
@Override
public int sub(int x, int y) throws RemoteException {
Log.i(TAG, className+" add Thread Name: "+Thread.currentThread().getName());
int z = x-y;
return z;
}
};
}
Service Connection
// Return the stub as interface
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i(TAG, className + " onServiceConnected");
iCalculator = ICalculator.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
unbindService(serviceConnection);
}
};
GoalKicker.com – Android™ Notes for Professionals 974
Chapter 208: Android game development
A short introduction to creating a game on the Android platform using Java
Section 208.1: Game using Canvas and SurfaceView
This covers how you can create a basic 2D game using SurfaceView.
First, we need an activity:
public class GameLauncher extends AppCompatActivity {
private Game game;
@Override
public void onCreate(Bundle sis){
super.onCreate(sis);
game = new Game(GameLauncher.this);//Initialize the game instance
setContentView(game);//setContentView to the game surfaceview
//Custom XML files can also be used, and then retrieve the game instance using findViewById.
}
}
The activity also has to be declared in the Android Manifest.
Now for the game itself. First, we start by implementing a game thread:
public class Game extends SurfaceView implements SurfaceHolder.Callback, Runnable{
/**
* Holds the surface frame
*/
private SurfaceHolder holder;
/**
* Draw thread
*/
private Thread drawThread;
/**
* True when the surface is ready to draw
*/
private boolean surfaceReady = false;
/**
* Drawing thread flag
*/
private boolean drawingActive = false;
/**
* Time per frame for 60 FPS
*/
private static final int MAX_FRAME_TIME = (int) (1000.0 / 60.0);
private static final String LOGTAG = "surface";
/*
GoalKicker.com – Android™ Notes for Professionals 975
* All the constructors are overridden to ensure functionality if one of the different
constructors are used through an XML file or programmatically
*/
public Game(Context context) {
super(context);
init();
}
public Game(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public Game(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@TargetApi(21)
public Game(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public void init(Context c) {
this.c = c;
SurfaceHolder holder = getHolder();
holder.addCallback(this);
setFocusable(true);
//Initialize other stuff here later
}
public void render(Canvas c){
//Game rendering here
}
public void tick(){
//Game logic here
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
if (width == 0 || height == 0){
return;
}
// resize your UI
}
@Override
public void surfaceCreated(SurfaceHolder holder){
this.holder = holder;
if (drawThread != null){
Log.d(LOGTAG, "draw thread still active..");
drawingActive = false;
try{
drawThread.join();
} catch (InterruptedException e){}
}
surfaceReady = true;
startDrawThread();
GoalKicker.com – Android™ Notes for Professionals 976
Log.d(LOGTAG, "Created");
}
@Override
public void surfaceDestroyed(SurfaceHolder holder){
// Surface is not used anymore - stop the drawing thread
stopDrawThread();
// and release the surface
holder.getSurface().release();
this.holder = null;
surfaceReady = false;
Log.d(LOGTAG, "Destroyed");
}
@Override
public boolean onTouchEvent(MotionEvent event){
// Handle touch events
return true;
}
/**
* Stops the drawing thread
*/
public void stopDrawThread(){
if (drawThread == null){
Log.d(LOGTAG, "DrawThread is null");
return;
}
drawingActive = false;
while (true){
try{
Log.d(LOGTAG, "Request last frame");
drawThread.join(5000);
break;
} catch (Exception e) {
Log.e(LOGTAG, "Could not join with draw thread");
}
}
drawThread = null;
}
/**
* Creates a new draw thread and starts it.
*/
public void startDrawThread(){
if (surfaceReady && drawThread == null){
drawThread = new Thread(this, "Draw thread");
drawingActive = true;
drawThread.start();
}
}
@Override
public void run() {
Log.d(LOGTAG, "Draw thread started");
long frameStartTime;
long frameTime;
/*
* In order to work reliable on Nexus 7, we place ~500ms delay at the start of drawing thread
* (AOSP - Issue 58385)
GoalKicker.com – Android™ Notes for Professionals 977
*/
if (android.os.Build.BRAND.equalsIgnoreCase("google") &&
android.os.Build.MANUFACTURER.equalsIgnoreCase("asus") &&
android.os.Build.MODEL.equalsIgnoreCase("Nexus 7")) {
Log.w(LOGTAG, "Sleep 500ms (Device: Asus Nexus 7)");
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {}
}
while (drawing) {
if (sf == null) {
return;
}
frameStartTime = System.nanoTime();
Canvas canvas = sf.lockCanvas();
if (canvas != null) {
try {
synchronized (sf) {
tick();
render(canvas);
}
} finally {
sf.unlockCanvasAndPost(canvas);
}
}
// calculate the time required to draw the frame in ms
frameTime = (System.nanoTime() - frameStartTime) / 1000000;
if (frameTime < MAX_FRAME_TIME){
try {
Thread.sleep(MAX_FRAME_TIME - frameTime);
} catch (InterruptedException e) {
// ignore
}
}
}
Log.d(LOGTAG, "Draw thread finished");
}
}
That is the basic part. Now you have the ability to draw onto the screen.
Now, let's start by adding to integers:
public final int x = 100;//The reason for this being static will be shown when the game is runnable
public int y;
public int velY;
For this next part, you are going to need an image. It should be about 100x100 but it can be bigger or smaller. For
learning, a Rect can also be used(but that requires change in code a little bit down)
Now, we declare a Bitmap:
private Bitmap PLAYER_BMP = BitmapFactory.decodeResource(getResources(),
R.drawable.my_player_drawable);
GoalKicker.com – Android™ Notes for Professionals 978
In render, we need to draw this bitmap.
...
c.drawBitmap(PLAYER_BMP, x, y, null);
...
BEFORE LAUNCHING there are still some things to be done
We need a boolean first:
boolean up = false;
in onTouchEvent, we add:
if(ev.getAction() == MotionEvent.ACTION_DOWN){
up = true;
}else if(ev.getAction() == MotionEvent.ACTION_UP){
up = false;
}
And in tick we need this to move the player:
if(up){
velY -=1;
}
else{
velY +=1;
}
if(velY >14)velY = 14;
if(velY <-14)velY = -14;
y += velY *2;
and now we need this in init:
WindowManager wm = (WindowManager) c.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point size = new Point();
display.getSize(size);
WIDTH = size.x;
HEIGHT = size.y;
y = HEIGHT/ 2 - PLAYER_BMP.getHeight();
And we need these to variables:
public static int WIDTH, HEIGHT;
At this point, the game is runnable. Meaning you can launch it and test it.
Now you should have a player image or rect going up and down the screen. The player can be created as a custom
class if needed. Then all the player-related things can be moved into that class, and use an instance of that class to
move, render and do other logic.
Now, as you probably saw under testing it flies off the screen. So we need to limit it.
First, we need to declare the Rect:
GoalKicker.com – Android™ Notes for Professionals 979
private Rect screen;
In init, after initializing width and height, we create a new rect that is the screen.
screen = new Rect(0,0,WIDTH,HEIGHT);
Now we need another rect in the form of a method:
private Rect getPlayerBound(){
return new Rect(x, y, x + PLAYER_BMP.getWidth(), y + PLAYER_BMP.getHeight();
}
and in tick:
if(!getPlayerBound().intersects(screen){
gameOver = true;
}
The implementation of gameOVer can also be used to show the start of a game.
Other aspects of a game worth noting:
Saving(currently missing in documentation)
GoalKicker.com – Android™ Notes for Professionals 980
Chapter 209: Android programming with
Kotlin
Using Kotlin with Android Studio is an easy task as Kotlin is developed by JetBrains. It is the same company that
stands behind IntelliJ IDEA - a base IDE for Android Studio. That is why there are almost none problems with the
compatibility.
Section 209.1: Installing the Kotlin plugin
First, you'll need to install the Kotlin plugin.
For Windows:
Navigate to File → Settings → Plugins → Install JetBrains plugin
For Mac:
Navigate to Android Studio → Preferences → Plugins → Install JetBrains plugin
And then search for and install Kotlin. You'll need to restart the IDE after this completes.
GoalKicker.com – Android™ Notes for Professionals 981
Section 209.2: Configuring an existing Gradle project with
Kotlin
You can create a New Project in Android Studio and then add Kotlin support to it or modify your existing project. To
do it, you have to:
1. Add dependency to a root gradle file - you have to add the dependency for kotlin-android plugin to a
root build.gradle file.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.2'
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
2. Apply Kotlin Android Plugin - simply add apply plugin: 'kotlin-android' to a module build.gradle file.
3. Add dependency to Kotlin stdlib - add the dependency to 'org.jetbrains.kotlin:kotlin-stdlib:1.1.2'
to the dependency section in a module build.gradle file.
For a new project, build.gradle file could looks like this:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "org.example.example"
minSdkVersion 16
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
GoalKicker.com – Android™ Notes for Professionals 982
compile 'org.jetbrains.kotlin:kotlin-stdlib:1.1.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
compile 'com.android.support:appcompat-v7:25.3.1'
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
testCompile 'junit:junit:4.12'
}
Section 209.3: Creating a new Kotlin Activity
1. Click to File → New → Kotlin Activity.
2. Choose a type of the Activity.
3. Select name and other parameter for the Activity.
4. Finish.
Final class could look like this:
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GoalKicker.com – Android™ Notes for Professionals 983
}
}
Section 209.4: Converting existing Java code to Kotlin
Kotlin Plugin for Android Studio support converting existing Java files to Kotlin files. Choose a Java file and invoke
action Convert Java File to Kotlin File:
Section 209.5: Starting a new Activity
fun startNewActivity(){
val intent: Intent = Intent(context, Activity::class.java)
startActivity(intent)
}
You can add extras to the intent just like in Java.
fun startNewActivityWithIntents(){
val intent: Intent = Intent(context, Activity::class.java)
intent.putExtra(KEY_NAME, KEY_VALUE)
startActivity(intent)
}
GoalKicker.com – Android™ Notes for Professionals 984
Chapter 210: Android-x86 in VirtualBox
The idea of this section is to cover how to install and use the VirtualBox with Android-x86 for debugging purposes.
This is a difficult task because there are differences between versions. For the moment I´m going to cover 6.0 which
is the one that I had to work with and then we'll have to find similarities.
It doesn't cover VirtualBox or a Linux in detail but it shows the commands I've used to make it work.
Section 210.1: Virtual hard drive Setup for SDCARD Support
With the virtual hard drive just created, boot the virtual machine with the android-x86 image in the optical drive.
Once you boot, you can see the grub menu of the Live CD
GoalKicker.com – Android™ Notes for Professionals 985
Choose the Debug Mode Option, then you should see the shell prompt. This is a busybox shell. You can get more
shell by switching between virtual console Alt-F1/F2/F3.
Create two partitions by fdisk (some other versions would use cfdisk). Format them to ext3. Then reboot:
# fdisk /dev/sda
Then type:
"n" (new partition)
"p" (primary partition)
"1" (1st partition)
"1" (first cylinder)
"261" (choose a cylinder, we'll leave 50% of the disk for a 2nd partition)
"2" (2nd partition)
"262" (262nd cylinder)
"522" (choose the last cylinder)
GoalKicker.com – Android™ Notes for Professionals 986
"w" (write the partition)
#mdev -s
#mke2fs -j -L DATA /dev/sda1
#mke2fs -j -L SDCARD /dev/sda2
#reboot -f
When you restart the virtual machine and the grub menu appears and you will be able edit the kernel boot line so
you can add DATA=sda1 SDCARD=sda2 options to point to the sdcard or the data partition.
Section 210.2: Installation in partition
With the virtual hard drive just created, boot the virtual machine with the android-x86 image as the optical drive.
In the booting options of the Live CD choose "Installation - Install Android to hard disk"
Choose the sda1 partition and install android and we'll install grub.
Reboot the virtual machine but make sure that the image is not in the optical drive so it can restart from the virtual
hard drive.
GoalKicker.com – Android™ Notes for Professionals 987
In the grub menu we need to edit kernel like in the "Android-x86 6.0-r3" option so press e.
Then we substitute "quiet" with "vga=ask" and add the option "SDCARD=sda2"
In my case, the kernel line looks like this after modified:
GoalKicker.com – Android™ Notes for Professionals 988
kenel /android-6.0-r3/kernel vga=ask root=ram0 SRC=/android-6/android-6.0-r3 SDCARD=sda2
Press b to boot, then you'll be able to choose the screen size pressing ENTER (the vga=ask option)
Once the installation wizard has started choose the language. I could choose English (United States) and Spanish
(United States) and I had trouble choosing any other.
Section 210.3: Virtual Machine setup
These are my VirtualBox settings:
OS Type: Linux 2.6 (I've user 64bit because my computer can support it)
Virtual hard drive size: 4Gb
Ram Memory: 2048
Video Memory: 8M
Sound device: Sound Blaster 16.
Network device: PCnet-Fast III, attached to NAT. You can also use bridged adapter, but you need a DHCP
server in your environment.
The image used with this configuration has been android-x86_64-6.0-r3.iso (it is 64bit) downloaded from
http://www.android-x86.org/download. I suppose that it also works with 32bit version.
GoalKicker.com – Android™ Notes for Professionals 989
Chapter 211: Leakcanary
Leak Canary is an Android and Java library used to detect leak in the application
Section 211.1: Implementing a Leak Canary in Android
Application
In your build.gradle you need to add the below dependencies:
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
In your Application class you need to add the below code inside your onCreate():
LeakCanary.install(this);
That's all you need to do for LeakCanary, it will automatically show notifications when there is a leak in your build.
GoalKicker.com – Android™ Notes for Professionals 990
Chapter 212: Okio
Section 212.1: Download / Implement
Download the latest JAR or grab via Maven:
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.12.0</version>
</dependency>
or Gradle:
compile 'com.squareup.okio:okio:1.12.0'
Section 212.2: PNG decoder
Decoding the chunks of a PNG file demonstrates Okio in practice.
private static final ByteString PNG_HEADER = ByteString.decodeHex("89504e470d0a1a0a");
public void decodePng(InputStream in) throws IOException {
try (BufferedSource pngSource = Okio.buffer(Okio.source(in))) {
ByteString header = pngSource.readByteString(PNG_HEADER.size());
if (!header.equals(PNG_HEADER)) {
throw new IOException("Not a PNG.");
}
while (true) {
Buffer chunk = new Buffer();
// Each chunk is a length, type, data, and CRC offset.
int length = pngSource.readInt();
String type = pngSource.readUtf8(4);
pngSource.readFully(chunk, length);
int crc = pngSource.readInt();
decodeChunk(type, chunk);
if (type.equals("IEND")) break;
}
}
}
private void decodeChunk(String type, Buffer chunk) {
if (type.equals("IHDR")) {
int width = chunk.readInt();
int height = chunk.readInt();
System.out.printf("%08x: %s %d x %d%n", chunk.size(), type, width, height);
} else {
System.out.printf("%08x: %s%n", chunk.size(), type);
}
}
Section 212.3: ByteStrings and Buers
ByteStrings and Buffers
GoalKicker.com – Android™ Notes for Professionals 991
Okio is built around two types that pack a lot of capability into a straightforward API:
ByteString is an immutable sequence of bytes. For character data, String is fundamental. ByteString is String's longlost
brother, making it easy to treat binary data as a value. This class is ergonomic: it knows how to encode and
decode itself as hex, base64, and UTF-8.
Buffer is a mutable sequence of bytes. Like ArrayList, you don't need to size your buffer in advance. You read and
write buffers as a queue: write data to the end and read it from the front. There's no obligation to manage
positions, limits, or capacities.
Internally, ByteString and Buffer do some clever things to save CPU and memory. If you encode a UTF-8 string as
a ByteString, it caches a reference to that string so that if you decode it later, there's no work to do.
Buffer is implemented as a linked list of segments. When you move data from one buffer to another, it reassigns
ownership of the segments rather than copying the data across. This approach is particularly helpful for
multithreaded programs: a thread that talks to the network can exchange data with a worker thread without any
copying or ceremony.
GoalKicker.com – Android™ Notes for Professionals 992
Chapter 213: Bluetooth Low Energy
This documentation is meant as an enhancement over the original documentation and it will focus on the latest
Bluetooth LE API introduced in Android 5.0 (API 21). Both Central and Peripheral roles will be covered as well as
how to start scanning and advertising operations.
Section 213.1: Finding BLE Devices
The following permissions are required to use the Bluetooth APIs:
android.permission.BLUETOOTH android.permission.BLUETOOTH_ADMIN
If you're targeting devices with Android 6.0 (API Level 23) or higher and want to perform scanning/advertising
operations you will require a Location permission:
android.permission.ACCESS_FINE_LOCATION
or
android.permission.ACCESS_COARSE_LOCATION
Note.- Devices with Android 6.0 (API Level 23) or higher also need to have Location Services enabled.
A BluetoothAdapter object is required to start scanning/advertising operations:
BluetoothManager bluetoothManager = (BluetoothManager)
context.getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
The startScan (ScanCallback callback)method of the BluetoothLeScanner class is the most basic way to start a
scanning operation. A ScanCallback object is required to receive results:
bluetoothAdapter.getBluetoothLeScanner().startScan(new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
Log.i(TAG, "Remote device name: " + result.getDevice().getName());
}
});
Section 213.2: Connecting to a GATT Server
Once you have discovered a desired BluetoothDevice object, you can connect to it by using its connectGatt()
method which takes as parameters a Context object, a boolean indicating whether to automatically connect to the
BLE device and a BluetoothGattCallback reference where connection events and client operations results will be
delivered:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
device.connectGatt(context, false, bluetoothGattCallback, BluetoothDevice.TRANSPORT_AUTO);
} else {
device.connectGatt(context, false, bluetoothGattCallback);
}
Override onConnectionStateChange in BluetoothGattCallback to receive connection an disconnection events:
GoalKicker.com – Android™ Notes for Professionals 993
BluetoothGattCallback bluetoothGattCallback =
new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.i(TAG, "Connected to GATT server.");
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.i(TAG, "Disconnected from GATT server.");
}
}
};
Section 213.3: Writing and Reading from Characteristics
Once you are connected to a Gatt Server, you're going to be interacting with it by writing and reading from the
server's characteristics. To do this, first you have to discover what services are available on this server and which
characteristics are available in each service:
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.i(TAG, "Connected to GATT server.");
gatt.discoverServices();
}
. . .
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
List<BluetoothGattService> services = gatt.getServices();
for (BluetoothGattService service : services) {
List<BluetoothGattCharacteristic> characteristics =
service.getCharacteristics();
for (BluetoothGattCharacteristic characteristic : characteristics) {
///Once you have a characteristic object, you can perform read/write
//operations with it
}
}
}
}
A basic write operation goes like this:
characteristic.setValue(newValue);
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
gatt.writeCharacteristic(characteristic);
When the write process has finished, the onCharacteristicWrite method of your BluetoothGattCallback will be
called:
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic
characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
GoalKicker.com – Android™ Notes for Professionals 994
Log.d(TAG, "Characteristic " + characteristic.getUuid() + " written);
}
A basic write operation goes like this:
gatt.readCharacteristic(characteristic);
When the write process has finished, the onCharacteristicRead method of your BluetoothGattCallback will be
called:
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
int status) {
super.onCharacteristicRead(gatt, characteristic, status);
byte[] value = characteristic.getValue();
}
Section 213.4: Subscribing to Notifications from the Gatt
Server
You can request to be notified from the Gatt Server when the value of a characteristic has been changed:
gatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
All notifications from the server will be received in the onCharacteristicChanged method of your
BluetoothGattCallback:
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic
characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
byte[] newValue = characteristic.getValue();
}
Section 213.5: Advertising a BLE Device
You can use Bluetooth LE Advertising to broadcast data packages to all nearby devices without having to establish a
connection first. Bear in mind that there's a strict limit of 31 bytes of advertisement data. Advertising your device is
also the first step towards letting other users connect to you.
Since not all devices support Bluetooth LE Advertising, the first step is to check that your device has all the
necessary requirements to support it. Afterwards, you can initialize a BluetoothLeAdvertiser object and with it,
you can start advertising operations:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
bluetoothAdapter.isMultipleAdvertisementSupported())
{
BluetoothLeAdvertiser advertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder();
//Define a service UUID according to your needs
dataBuilder.addServiceUuid(SERVICE_UUID);
GoalKicker.com – Android™ Notes for Professionals 995
dataBuilder.setIncludeDeviceName(true);
AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder();
settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER);
settingsBuilder.setTimeout(0);
//Use the connectable flag if you intend on opening a Gatt Server
//to allow remote connections to your device.
settingsBuilder.setConnectable(true);
AdvertiseCallback advertiseCallback=new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
super.onStartSuccess(settingsInEffect);
Log.i(TAG, "onStartSuccess: ");
}
@Override
public void onStartFailure(int errorCode) {
super.onStartFailure(errorCode);
Log.e(TAG, "onStartFailure: "+errorCode );
}
};
advertising.startAdvertising(settingsBuilder.build(),dataBuilder.build(),advertiseCallback);
}
Section 213.6: Using a Gatt Server
In order for your device to act as a peripheral, first you need to open a BluetoothGattServer and populate it with
at least one BluetoothGattService and one BluetoothGattCharacteristic:
BluetoothGattServer server=bluetoothManager.openGattServer(context, bluetoothGattServerCallback);
BluetoothGattService service = new BluetoothGattService(SERVICE_UUID,
BluetoothGattService.SERVICE_TYPE_PRIMARY);
This is an example of a BluetoothGattCharacteristic with full write,read and notify permissions. According to your
needs, you might want to fine tune the permissions that you grant this characteristic:
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_UUID,
BluetoothGattCharacteristic.PROPERTY_READ |
BluetoothGattCharacteristic.PROPERTY_WRITE |
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ |
BluetoothGattCharacteristic.PERMISSION_WRITE);
characteristic.addDescriptor(new
BluetoothGattDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
BluetoothGattCharacteristic.PERMISSION_WRITE));
service.addCharacteristic(characteristic);
server.addService(service);
The BluetoothGattServerCallback is responsible for receiving all events related to your BluetoothGattServer:
BluetoothGattServerCallback bluetoothGattServerCallback= new BluetoothGattServerCallback() {
@Override
GoalKicker.com – Android™ Notes for Professionals 996
public void onConnectionStateChange(BluetoothDevice device, int status, int
newState) {
super.onConnectionStateChange(device, status, newState);
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int
offset, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
}
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int
offset, byte[] value) {
super.onCharacteristicWriteRequest(device, requestId, characteristic,
preparedWrite, responseNeeded, offset, value);
}
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int
offset, BluetoothGattDescriptor descriptor) {
super.onDescriptorReadRequest(device, requestId, offset, descriptor);
}
@Override
public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset,
byte[] value) {
super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite,
responseNeeded, offset, value);
}
Whenever you receive a request for a write/read to a characteristic or descriptor you must send a response to it in
order for the request to be completed succesfully :
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
server.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, YOUR_RESPONSE);
}
GoalKicker.com – Android™ Notes for Professionals 997
Chapter 214: Looper
A Looper is an Android class used to run a message loop for a thread, which usually do not have one associated
with them.
The most common Looper in Android is the main-loop, also commonly known as the main-thread. This instance is
unique for an application and can be accessed statically with Looper.getMainLooper().
If a Looper is associated with the current thread, it can be retrieved with Looper.myLooper().
Section 214.1: Create a simple LooperThread
A typical example of the implementation of a Looper thread given by the official documentation uses
Looper.prepare() and Looper.loop() and associates a Handler with the loop between these calls.
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
Section 214.2: Run a loop with a HandlerThread
A HandlerThread can be used to start a thread with a Looper. This looper then can be used to create a Handler for
communications with it.
HandlerThread thread = new HandlerThread("thread-name");
thread.start();
Handler handler = new Handler(thread.getLooper());
GoalKicker.com – Android™ Notes for Professionals 998
Chapter 215: Annotation Processor
Annotation processor is a tool build in javac for scanning and processing annotations at compile time.
Annotations are a class of metadata that can be associated with classes, methods, fields, and even other
annotations.There are two ways to access these annotations at runtime via reflection and at compile time via
annotation processors.
Section 215.1: @NonNull Annotation
public class Foo {
private String name;
public Foo(@NonNull String name){...};
...
}
Here @NonNull is annotation which is processed compile time by the android studio to warn you that the particular
function needs non null parameter.
Section 215.2: Types of Annotations
There are three types of annotations.
1. Marker Annotation - annotation that has no method
@interface CustomAnnotation {}
2. Single-Value Annotation - annotation that has one method
@interface CustomAnnotation {
int value();
}
3. Multi-Value Annotation - annotation that has more than one method
@interface CustomAnnotation{
int value1();
String value2();
String value3();
}
Section 215.3: Creating and Using Custom Annotations
For creating custom annotations we need to decide
Target - on which these annotations will work on like field level, method level, type level etc.
Retention - to what level annotation will be available.
For this, we have built in custom annotations. Check out these mostly used ones:
@Target
GoalKicker.com – Android™ Notes for Professionals 999
@Retention
Creating Custom Annotation
@Retention(RetentionPolicy.SOURCE) // will not be available in compiled class
@Target(ElementType.METHOD) // can be applied to methods only
@interface CustomAnnotation{
int value();
}
Using Custom Annotation
class Foo{
@CustomAnnotation(value = 1) // will be used by an annotation processor
public void foo(){..}
}
the value provided inside @CustomAnnotation will be consumed by an Annotationprocessor may be to generate
code at compile time etc.
GoalKicker.com – Android™ Notes for Professionals 1000
Chapter 216: SyncAdapter with periodically
do sync of data
The sync adapter component in your app encapsulates the code for the tasks that transfer data between the device
and a server. Based on the scheduling and triggers you provide in your app, the sync adapter framework runs the
code in the sync adapter component.
Recently i worked on SyncAdapter i want share my knowledge with others,it may help others.
Section 216.1: Sync adapter with every min requesting value
from server
<provider
android:name=".DummyContentProvider"
android:authorities="sample.map.com.ipsyncadapter"
android:exported="false" />
<!-- This service implements our SyncAdapter. It needs to be exported, so that the system
sync framework can access it. -->
<service android:name=".SyncService"
android:exported="true">
<!-- This intent filter is required. It allows the system to launch our sync service
as needed. -->
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<!-- This points to a required XML file which describes our SyncAdapter. -->
<meta-data android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>
<!-- This implements the account we'll use as an attachment point for our SyncAdapter. Since
our SyncAdapter doesn't need to authenticate the current user (it just fetches a public RSS
feed), this account's implementation is largely empty.
It's also possible to attach a SyncAdapter to an existing account provided by another
package. In that case, this element could be omitted here. -->
<service android:name=".AuthenticatorService"
>
<!-- Required filter used by the system to launch our account service. -->
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<!-- This points to an XMLf ile which describes our account service. -->
<meta-data android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
This code need to be add in manifest file
In above code we have the syncservice and conteprovider and authenticatorservice.
In app we need to create the xml package to add syncadpter and authenticator xml files. authenticator.xml
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/R.String.accountType"
android:icon="@mipmap/ic_launcher"
GoalKicker.com – Android™ Notes for Professionals 1001
android:smallIcon="@mipmap/ic_launcher"
android:label="@string/app_name"
/>
syncadapter
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="@string/R.String.contentAuthority"
android:accountType="@string/R.String.accountType"
android:userVisible="true"
android:allowParallelSyncs="true"
android:isAlwaysSyncable="true"
android:supportsUploading="false"/>
Authenticator
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.os.Bundle;
public class Authenticator extends AbstractAccountAuthenticator {
private Context mContext;
public Authenticator(Context context) {
super(context);
this.mContext=context;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse accountAuthenticatorResponse, String
s) {
return null;
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s,
String s1, String[] strings, Bundle bundle) throws NetworkErrorException {
return null;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse,
Account account, Bundle bundle) throws NetworkErrorException {
return null;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse, Account
account, String s, Bundle bundle) throws NetworkErrorException {
return null;
}
@Override
public String getAuthTokenLabel(String s) {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse,
GoalKicker.com – Android™ Notes for Professionals 1002
Account account, String s, Bundle bundle) throws NetworkErrorException {
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse accountAuthenticatorResponse, Account
account, String[] strings) throws NetworkErrorException {
return null;
}
}
AuthenticatorService
public class AuthenticatorService extends Service {
private Authenticator authenticator;
public AuthenticatorService() {
super();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
IBinder ret = null;
if (intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) ;
ret = getAuthenticator().getIBinder();
return ret;
}
public Authenticator getAuthenticator() {
if (authenticator == null)
authenticator = new Authenticator(this);
return authenticator;
}
}
IpDataDBHelper
public class IpDataDBHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION=1;
private static final String DATABASE_NAME="ip.db";
public static final String TABLE_IP_DATA="ip";
public static final String COLUMN_ID="_id";
public static final String COLUMN_IP="ip";
public static final String COLUMN_COUNTRY_CODE="country_code";
public static final String COLUMN_COUNTRY_NAME="country_name";
public static final String COLUMN_CITY="city";
public static final String COLUMN_LATITUDE="latitude";
public static final String COLUMN_LONGITUDE="longitude";
public IpDataDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int
version) {
super(context, DATABASE_NAME, factory, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
String CREATE_TABLE="CREATE TABLE " + TABLE_IP_DATA + "( " + COLUMN_ID + " INTEGER PRIMARY
KEY ,"
GoalKicker.com – Android™ Notes for Professionals 1003
+ COLUMN_IP + " INTEGER ," + COLUMN_COUNTRY_CODE + " INTEGER ," +
COLUMN_COUNTRY_NAME +
" TEXT ," + COLUMN_CITY + " TEXT ," + COLUMN_LATITUDE + " INTEGER ," +
COLUMN_LONGITUDE + " INTEGER)";
sqLiteDatabase.execSQL(CREATE_TABLE);
Log.d("SQL",CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + TABLE_IP_DATA);
onCreate(sqLiteDatabase);
}
public long AddIPData(ContentValues values)
{
SQLiteDatabase sqLiteDatabase =getWritableDatabase();
long insertedRow=sqLiteDatabase.insert(TABLE_IP_DATA,null,values);
return insertedRow;
}
public Cursor getAllIpData()
{
String[]
projection={COLUMN_ID,COLUMN_IP,COLUMN_COUNTRY_CODE,COLUMN_COUNTRY_NAME,COLUMN_CITY,COLUMN_LATITUDE
,COLUMN_LONGITUDE};
SQLiteDatabase sqLiteDatabase =getReadableDatabase();
Cursor cursor = sqLiteDatabase.query(TABLE_IP_DATA,projection,null,null,null,null,null);
return cursor;
}
public int deleteAllIpData()
{
SQLiteDatabase sqLiteDatabase=getWritableDatabase();
int rowDeleted=sqLiteDatabase.delete(TABLE_IP_DATA,null,null);
return rowDeleted;
}
}
MainActivity
public class MainActivity extends AppCompatActivity {
private static final String ACCOUNT_TYPE="sample.map.com.ipsyncadapter";
private static final String AUTHORITY="sample.map.com.ipsyncadapter";
private static final String ACCOUNT_NAME="Sync";
public TextView mIp,mCountryCod,mCountryName,mCity,mLatitude,mLongitude;
CursorAdapter cursorAdapter;
Account mAccount;
private String TAG=this.getClass().getCanonicalName();
ListView mListView;
public SharedPreferences mSharedPreferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.list);
mIp=(TextView)findViewById(R.id.txt_ip);
mCountryCod=(TextView)findViewById(R.id.txt_country_code);
mCountryName=(TextView)findViewById(R.id.txt_country_name);
GoalKicker.com – Android™ Notes for Professionals 1004
mCity=(TextView)findViewById(R.id.txt_city);
mLatitude=(TextView)findViewById(R.id.txt_latitude);
mLongitude=(TextView)findViewById(R.id.txt_longitude);
mSharedPreferences=getSharedPreferences("MyIp",0);
//Using shared preference iam displaying values in text view.
String txtIp=mSharedPreferences.getString("ipAdr","");
String txtCC=mSharedPreferences.getString("CCode","");
String txtCN=mSharedPreferences.getString("CName","");
String txtC=mSharedPreferences.getString("City","");
String txtLP=mSharedPreferences.getString("Latitude","");
String txtLN=mSharedPreferences.getString("Longitude","");
mIp.setText(txtIp);
mCountryCod.setText(txtCC);
mCountryName.setText(txtCN);
mCity.setText(txtC);
mLatitude.setText(txtLP);
mLongitude.setText(txtLN);
mAccount=createSyncAccount(this);
//In this code i am using content provider to save data.
/* Cursor
cursor=getContentResolver().query(MyIPContentProvider.CONTENT_URI,null,null,null,null);
cursorAdapter=new SimpleCursorAdapter(this,R.layout.list_item,cursor,new String
[]{"ip","country_code","country_name","city","latitude","longitude"},
new int[]
{R.id.txt_ip,R.id.txt_country_code,R.id.txt_country_name,R.id.txt_city,R.id.txt_latitude,R.id.txt_lon
gitude},0);
mListView.setAdapter(cursorAdapter);
getContentResolver().registerContentObserver(MyIPContentProvider.CONTENT_URI,true,new
StockContentObserver(new Handler()));
*/
Bundle settingBundle=new Bundle();
settingBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL,true);
settingBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED,true);
ContentResolver.requestSync(mAccount,AUTHORITY,settingBundle);
ContentResolver.setSyncAutomatically(mAccount,AUTHORITY,true);
ContentResolver.addPeriodicSync(mAccount,AUTHORITY,Bundle.EMPTY,60);
}
private Account createSyncAccount(MainActivity mainActivity) {
Account account=new Account(ACCOUNT_NAME,ACCOUNT_TYPE);
AccountManager
accountManager=(AccountManager)mainActivity.getSystemService(ACCOUNT_SERVICE);
if(accountManager.addAccountExplicitly(account,null,null))
{
}else
{
}
return account;
}
private class StockContentObserver extends ContentObserver {
@Override
public void onChange(boolean selfChange, Uri uri) {
Log.d(TAG, "CHANGE OBSERVED AT URI: " + uri);
GoalKicker.com – Android™ Notes for Professionals 1005
cursorAdapter.swapCursor(getContentResolver().query(MyIPContentProvider.CONTENT_URI, null, null,
null, null));
}
public StockContentObserver(Handler handler) {
super(handler);
}
}
@Override
protected void onResume() {
super.onResume();
registerReceiver(syncStaredReceiver, new IntentFilter(SyncAdapter.SYNC_STARTED));
registerReceiver(syncFinishedReceiver, new IntentFilter(SyncAdapter.SYNC_FINISHED));
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(syncStaredReceiver);
unregisterReceiver(syncFinishedReceiver);
}
private BroadcastReceiver syncFinishedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Sync finished!");
Toast.makeText(getApplicationContext(), "Sync Finished",
Toast.LENGTH_SHORT).show();
}
};
private BroadcastReceiver syncStaredReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Sync started!");
Toast.makeText(getApplicationContext(), "Sync started...",
Toast.LENGTH_SHORT).show();
}
};
}
MyIPContentProvider
public class MyIPContentProvider extends ContentProvider {
public static final int IP_DATA=1;
private static final String AUTHORITY="sample.map.com.ipsyncadapter";
private static final String TABLE_IP_DATA="ip_data";
public static final Uri CONTENT_URI=Uri.parse("content://" + AUTHORITY + '/' + TABLE_IP_DATA);
private static final UriMatcher URI_MATCHER= new UriMatcher(UriMatcher.NO_MATCH);
static
{
URI_MATCHER.addURI(AUTHORITY,TABLE_IP_DATA,IP_DATA);
}
private IpDataDBHelper myDB;
@Override
public boolean onCreate() {
GoalKicker.com – Android™ Notes for Professionals 1006
myDB=new IpDataDBHelper(getContext(),null,null,1);
return false;
}
@Nullable
@Override
public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) {
int uriType=URI_MATCHER.match(uri);
Cursor cursor=null;
switch (uriType)
{
case IP_DATA:
cursor=myDB.getAllIpData();
break;
default:
throw new IllegalArgumentException("UNKNOWN URL");
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Nullable
@Override
public String getType(Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
int uriType=URI_MATCHER.match(uri);
long id=0;
switch (uriType)
{
case IP_DATA:
id=myDB.AddIPData(contentValues);
break;
default:
throw new IllegalArgumentException("UNKNOWN URI :" +uri);
}
getContext().getContentResolver().notifyChange(uri,null);
return Uri.parse(contentValues + "/" + id);
}
@Override
public int delete(Uri uri, String s, String[] strings) {
int uriType=URI_MATCHER.match(uri);
int rowsDeleted=0;
switch (uriType)
{
case IP_DATA:
rowsDeleted=myDB.deleteAllIpData();
break;
default:
throw new IllegalArgumentException("UNKNOWN URI :" +uri);
}
getContext().getContentResolver().notifyChange(uri,null);
return rowsDeleted;
}
@Override
GoalKicker.com – Android™ Notes for Professionals 1007
public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
return 0;
}
}
SyncAdapter
public class SyncAdapter extends AbstractThreadedSyncAdapter {
ContentResolver mContentResolver;
Context mContext;
public static final String SYNC_STARTED="Sync Started";
public static final String SYNC_FINISHED="Sync Finished";
private static final String TAG=SyncAdapter.class.getCanonicalName();
public SharedPreferences mSharedPreferences;
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
this.mContext=context;
mContentResolver=context.getContentResolver();
Log.i("SyncAdapter","SyncAdapter");
}
@Override
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient
contentProviderClient, SyncResult syncResult) {
Intent intent = new Intent(SYNC_STARTED);
mContext.sendBroadcast(intent);
Log.i(TAG, "onPerformSync");
intent = new Intent(SYNC_FINISHED);
mContext.sendBroadcast(intent);
mSharedPreferences =mContext.getSharedPreferences("MyIp",0);
SharedPreferences.Editor editor=mSharedPreferences.edit();
mContentResolver.delete(MyIPContentProvider.CONTENT_URI,null,null);
String data="";
try {
URL url =new URL("https://freegeoip.net/json/");
Log.d(TAG, "URL :"+url);
HttpURLConnection connection=(HttpURLConnection)url.openConnection();
Log.d(TAG,"Connection :"+connection);
connection.connect();
Log.d(TAG,"Connection 1:"+connection);
InputStream inputStream=connection.getInputStream();
data=getInputData(inputStream);
Log.d(TAG,"Data :"+data);
if (data != null || !data.equals("null")) {
JSONObject jsonObject = new JSONObject(data);
String ipa = jsonObject.getString("ip");
String country_code = jsonObject.getString("country_code");
String country_name = jsonObject.getString("country_name");
String region_code=jsonObject.getString("region_code");
String region_name=jsonObject.getString("region_name");
GoalKicker.com – Android™ Notes for Professionals 1008
String zip_code=jsonObject.getString("zip_code");
String time_zone=jsonObject.getString("time_zone");
String metro_code=jsonObject.getString("metro_code");
String city = jsonObject.getString("city");
String latitude = jsonObject.getString("latitude");
String longitude = jsonObject.getString("longitude");
/* ContentValues values = new ContentValues();
values.put("ip", ipa);
values.put("country_code", country_code);
values.put("country_name", country_name);
values.put("city", city);
values.put("latitude", latitude);
values.put("longitude", longitude);*/
//Using cursor adapter for results.
//mContentResolver.insert(MyIPContentProvider.CONTENT_URI, values);
//Using Shared preference for results.
editor.putString("ipAdr",ipa);
editor.putString("CCode",country_code);
editor.putString("CName",country_name);
editor.putString("City",city);
editor.putString("Latitude",latitude);
editor.putString("Longitude",longitude);
editor.commit();
}
}catch(Exception e){
e.printStackTrace();
}
}
private String getInputData(InputStream inputStream) throws IOException {
StringBuilder builder=new StringBuilder();
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
//String data=null;
/*Log.d(TAG,"Builder 2:"+ bufferedReader.readLine());
while ((data=bufferedReader.readLine())!= null);
{
builder.append(data);
Log.d(TAG,"Builder :"+data);
}
Log.d(TAG,"Builder 1 :"+data);
bufferedReader.close();*/
String data=bufferedReader.readLine();
bufferedReader.close();
return data.toString();
}
}
SyncService
public class SyncService extends Service {
private static SyncAdapter syncAdapter=null;
private static final Object syncAdapterLock=new Object();
@Override
public void onCreate() {
synchronized (syncAdapterLock)
GoalKicker.com – Android™ Notes for Professionals 1009
{
if(syncAdapter==null)
{
syncAdapter =new SyncAdapter(getApplicationContext(),true);
}
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return syncAdapter.getSyncAdapterBinder();
}
}
GoalKicker.com – Android™ Notes for Professionals 1010
Chapter 217: Fastjson
Fastjson is a Java library that can be used to convert Java Objects into their JSON representation. It can also be used
to convert a JSON string to an equivalent Java object.
Fastjson Features:
Provide best performance in server side and android client
Provide simple toJSONString() and parseObject() methods to convert Java objects to JSON and vice-versa
Allow pre-existing unmodifiable objects to be converted to and from JSON
Extensive support of Java Generics
Section 217.1: Parsing JSON with Fastjson
You can look at example in Fastjson library
Encode
import com.alibaba.fastjson.JSON;
Group group = new Group();
group.setId(0L);
group.setName("admin");
User guestUser = new User();
guestUser.setId(2L);
guestUser.setName("guest");
User rootUser = new User();
rootUser.setId(3L);
rootUser.setName("root");
group.addUser(guestUser);
group.addUser(rootUser);
String jsonString = JSON.toJSONString(group);
System.out.println(jsonString);
Output
{"id":0,"name":"admin","users":[{"id":2,"name":"guest"},{"id":3,"name":"root"}]}
Decode
String jsonString = ...;
Group group = JSON.parseObject(jsonString, Group.class);
Group.java
public class Group {
private Long id;
GoalKicker.com – Android™ Notes for Professionals 1011
private String name;
private List<User> users = new ArrayList<User>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
public void addUser(User user) {
users.add(user);
}
}
User.java
public class User {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Section 217.2: Convert the data of type Map to JSON String
Code
GoalKicker.com – Android™ Notes for Professionals 1012
Group group = new Group();
group.setId(1);
group.setName("Ke");
User user1 = new User();
user1.setId(2);
user1.setName("Liu");
User user2 = new User();
user2.setId(3);
user2.setName("Yue");
group.getList().add(user1);
group.getList().add(user2);
Map<Integer, Object> map = new HashMap<Integer,Object>();
map.put(1, "No.1");
map.put(2, "No.2");
map.put(3, group.getList());
String jsonString = JSON.toJSONString(map);
System.out.println(jsonString);
Output
{1:"No.1",2:"No.2",3:[{"id":2,"name":"Liu"},{"id":3,"name":"Yue"}]}
GoalKicker.com – Android™ Notes for Professionals 1013
Chapter 218: JSON in Android with org.json
Section 218.1: Creating a simple JSON object
Create the JSONObject using the empty constructor and add fields using the put() method, which is overloaded so
that it can be used with different types:
try {
// Create a new instance of a JSONObject
final JSONObject object = new JSONObject();
// With put you can add a name/value pair to the JSONObject
object.put("name", "test");
object.put("content", "Hello World!!!1");
object.put("year", 2016);
object.put("value", 3.23);
object.put("member", true);
object.put("null_value", JSONObject.NULL);
// Calling toString() on the JSONObject returns the JSON in string format.
final String json = object.toString();
} catch (JSONException e) {
Log.e(TAG, "Failed to create JSONObject", e);
}
The resulting JSON string looks like this:
{
"name":"test",
"content":"Hello World!!!1",
"year":2016,
"value":3.23,
"member":true,
"null_value":null
}
Section 218.2: Create a JSON String with null value
If you need to produce a JSON string with a value of null like this:
{
"name":null
}
Then you have to use the special constant JSONObject.NULL.
Functioning example:
jsonObject.put("name", JSONObject.NULL);
Section 218.3: Add JSONArray to JSONObject
// Create a new instance of a JSONArray
JSONArray array = new JSONArray();
GoalKicker.com – Android™ Notes for Professionals 1014
// With put() you can add a value to the array.
array.put("ASDF");
array.put("QWERTY");
// Create a new instance of a JSONObject
JSONObject obj = new JSONObject();
try {
// Add the JSONArray to the JSONObject
obj.put("the_array", array);
} catch (JSONException e) {
e.printStackTrace();
}
String json = obj.toString();
The resulting JSON string looks like this:
{
"the_array":[
"ASDF",
"QWERTY"
]
}
Section 218.4: Parse simple JSON object
Consider the following JSON string:
{
"title": "test",
"content": "Hello World!!!",
"year": 2016,
"names" : [
"Hannah",
"David",
"Steve"
]
}
This JSON object can be parsed using the following code:
try {
// create a new instance from a string
JSONObject jsonObject = new JSONObject(jsonAsString);
String title = jsonObject.getString("title");
String content = jsonObject.getString("content");
int year = jsonObject.getInt("year");
JSONArray names = jsonObject.getJSONArray("names"); //for an array of String objects
} catch (JSONException e) {
Log.w(TAG,"Could not parse JSON. Error: " + e.getMessage());
}
Here is another example with a JSONArray nested inside JSONObject:
{
"books":[
{
"title":"Android JSON Parsing",
GoalKicker.com – Android™ Notes for Professionals 1015
"times_sold":186
}
]
}
This can be parsed with the following code:
JSONObject root = new JSONObject(booksJson);
JSONArray booksArray = root.getJSONArray("books");
JSONObject firstBook = booksArray.getJSONObject(0);
String title = firstBook.getString("title");
int timesSold = firstBook.getInt("times_sold");
Section 218.5: Check for the existence of fields on JSON
Sometimes it's useful to check if a field is present or absent on your JSON to avoid some JSONException on your
code.
To achieve that, use the JSONObject#has(String) or the method, like on the following example:
Sample JSON
{
"name":"James"
}
Java code
String jsonStr = " { \"name\":\"James\" }";
JSONObject json = new JSONObject(jsonStr);
// Check if the field "name" is present
String name, surname;
// This will be true, since the field "name" is present on our JSON.
if (json.has("name")) {
name = json.getString("name");
}
else {
name = "John";
}
// This will be false, since our JSON doesn't have the field "surname".
if (json.has("surname")) {
surname = json.getString("surname");
}
else {
surname = "Doe";
}
// Here name == "James" and surname == "Doe".
Section 218.6: Create nested JSON object
To produce nested JSON object, you need to simply add one JSON object to another:
JSONObject mainObject = new JSONObject(); // Host object
JSONObject requestObject = new JSONObject(); // Included object
try {
GoalKicker.com – Android™ Notes for Professionals 1016
requestObject.put("lastname", lastname);
requestObject.put("phone", phone);
requestObject.put("latitude", lat);
requestObject.put("longitude", lon);
requestObject.put("theme", theme);
requestObject.put("text", message);
mainObject.put("claim", requestObject);
} catch (JSONException e) {
return "JSON Error";
}
Now mainObject contains a key called claim with the whole requestObject as a value.
Section 218.7: Updating the elements in the JSON
sample json to update
{
"student":{"name":"Rahul", "lastname":"sharma"},
"marks":{"maths":"88"}
}
To update the elements value in the json we need to assign the value and update.
try {
// Create a new instance of a JSONObject
final JSONObject object = new JSONObject(jsonString);
JSONObject studentJSON = object.getJSONObject("student");
studentJSON.put("name","Kumar");
object.remove("student");
object.put("student",studentJSON);
// Calling toString() on the JSONObject returns the JSON in string format.
final String json = object.toString();
} catch (JSONException e) {
Log.e(TAG, "Failed to create JSONObject", e);
}
updated value
{
"student":{"name":"Kumar", "lastname":"sharma"},
"marks":{"maths":"88"}
}
Section 218.8: Using JsonReader to read JSON from a stream
JsonReader reads a JSON encoded value as a stream of tokens.
public List<Message> readJsonStream(InputStream in) throws IOException {
JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
try {
return readMessagesArray(reader);
GoalKicker.com – Android™ Notes for Professionals 1017
} finally {
reader.close();
}
}
public List<Message> readMessagesArray(JsonReader reader) throws IOException {
List<Message> messages = new ArrayList<Message>();
reader.beginArray();
while (reader.hasNext()) {
messages.add(readMessage(reader));
}
reader.endArray();
return messages;
}
public Message readMessage(JsonReader reader) throws IOException {
long id = -1;
String text = null;
User user = null;
List<Double> geo = null;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
if (name.equals("id")) {
id = reader.nextLong();
} else if (name.equals("text")) {
text = reader.nextString();
} else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
geo = readDoublesArray(reader);
} else if (name.equals("user")) {
user = readUser(reader);
} else {
reader.skipValue();
}
}
reader.endObject();
return new Message(id, text, user, geo);
}
public List<Double> readDoublesArray(JsonReader reader) throws IOException {
List<Double> doubles = new ArrayList<Double>();
reader.beginArray();
while (reader.hasNext()) {
doubles.add(reader.nextDouble());
}
reader.endArray();
return doubles;
}
public User readUser(JsonReader reader) throws IOException {
String username = null;
int followersCount = -1;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
if (name.equals("name")) {
username = reader.nextString();
} else if (name.equals("followers_count")) {
GoalKicker.com – Android™ Notes for Professionals 1018
followersCount = reader.nextInt();
} else {
reader.skipValue();
}
}
reader.endObject();
return new User(username, followersCount);
}
Section 218.9: Working with null-string when parsing json
{
"some_string": null,
"ather_string": "something"
}
If we will use this way:
JSONObject json = new JSONObject(jsonStr);
String someString = json.optString("some_string");
We will have output:
someString = "null";
So we need to provide this workaround:
/**
* According to
http://stackoverflow.com/questions/18226288/json-jsonobject-optstring-returns-string-null
* we need to provide a workaround to opt string from json that can be null.
* <strong></strong>
*/
public static String optNullableString(JSONObject jsonObject, String key) {
return optNullableString(jsonObject, key, "");
}
/**
* According to
http://stackoverflow.com/questions/18226288/json-jsonobject-optstring-returns-string-null
* we need to provide a workaround to opt string from json that can be null.
* <strong></strong>
*/
public static String optNullableString(JSONObject jsonObject, String key, String fallback) {
if (jsonObject.isNull(key)) {
return fallback;
} else {
return jsonObject.optString(key, fallback);
}
}
And then call:
JSONObject json = new JSONObject(jsonStr);
String someString = optNullableString(json, "some_string");
String someString2 = optNullableString(json, "some_string", "");
And we will have Output as we expected:
GoalKicker.com – Android™ Notes for Professionals 1019
someString = null; //not "null"
someString2 = "";
Section 218.10: Handling dynamic key for JSON response
This is an example for how to handle dynamic key for response. Here A and B are dynamic keys it can be anything
Response
{
"response": [
{
"A": [
{
"name": "Tango"
},
{
"name": "Ping"
}
],
"B": [
{
"name": "Jon"
},
{
"name": "Mark"
}
]
}
]
}
Java code
// ResponseData is raw string of response
JSONObject responseDataObj = new JSONObject(responseData);
JSONArray responseArray = responseDataObj.getJSONArray("response");
for (int i = 0; i < responseArray.length(); i++) {
// Nodes ArrayList<ArrayList<String>> declared globally
nodes = new ArrayList<ArrayList<String>>();
JSONObject obj = responseArray.getJSONObject(i);
Iterator keys = obj.keys();
while(keys.hasNext()) {
// Loop to get the dynamic key
String currentDynamicKey = (String)keys.next();
// Get the value of the dynamic key
JSONArray currentDynamicValue = obj.getJSONArray(currentDynamicKey);
int jsonArraySize = currentDynamicValue.length();
if(jsonArraySize > 0) {
for (int ii = 0; ii < jsonArraySize; ii++) {
// NameList ArrayList<String> declared globally
nameList = new ArrayList<String>();
if(ii == 0) {
JSONObject nameObj = currentDynamicValue.getJSONObject(ii);
String name = nameObj.getString("name");
System.out.print("Name = " + name);
// Store name in an array list
nameList.add(name);
}
}
GoalKicker.com – Android™ Notes for Professionals 1020
}
nodes.add(nameList);
}
}
GoalKicker.com – Android™ Notes for Professionals 1021
Chapter 219: Gson
Gson is a Java library that can be used to convert Java Objects into their JSON representation. Gson considers both
of these as very important design goals.
Gson Features:
Provide simple toJson() and fromJson() methods to convert Java objects to JSON and vice-versa
Allow pre-existing unmodifiable objects to be converted to and from JSON
Extensive support of Java Generics
Support arbitrarily complex objects (with deep inheritance hierarchies and extensive use of generic types)
Section 219.1: Parsing JSON with Gson
The example shows parsing a JSON object using the Gson library from Google.
Parsing objects:
class Robot {
//OPTIONAL - this annotation allows for the key to be different from the field name, and can be
omitted if key and field name are same . Also this is good coding practice as it decouple your
variable names with server keys name
@SerializedName("version")
private String version;
@SerializedName("age")
private int age;
@SerializedName("robotName")
private String name;
// optional : Benefit it allows to set default values and retain them, even if key is missing
from Json response. Not required for primitive data types.
public Robot{
version = "";
name = "";
}
}
Then where parsing needs to occur, use the following:
String robotJson = "{
\"version\": \"JellyBean\",
\"age\": 3,
\"robotName\": \"Droid\"
}";
Gson gson = new Gson();
Robot robot = gson.fromJson(robotJson, Robot.class);
Parsing a list:
GoalKicker.com – Android™ Notes for Professionals 1022
When retrieving a list of JSON objects, often you will want to parse them and convert them into Java objects.
The JSON string that we will try to convert is the following:
{
"owned_dogs": [
{
"name": "Ron",
"age": 12,
"breed": "terrier"
},
{
"name": "Bob",
"age": 4,
"breed": "bulldog"
},
{
"name": "Johny",
"age": 3,
"breed": "golden retriever"
}
]
}
This particular JSON array contains three objects. In our Java code we'll want to map these objects to Dog objects. A
Dog object would look like this:
private class Dog {
public String name;
public int age;
@SerializedName("breed")
public String breedName;
}
To convert the JSON array to a Dog[]:
Dog[] arrayOfDogs = gson.fromJson(jsonArrayString, Dog[].class);
Converting a Dog[] to a JSON string:
String jsonArray = gson.toJson(arrayOfDogs, Dog[].class);
To convert the JSON array to an ArrayList<Dog> we can do the following:
Type typeListOfDogs = new TypeToken<List<Dog>>(){}.getType();
List<Dog> listOfDogs = gson.fromJson(jsonArrayString, typeListOfDogs);
The Type object typeListOfDogs defines what a list of Dog objects would look like. GSON can use this type object to
map the JSON array to the right values.
Alternatively, converting a List<Dog> to a JSON array can be done in a similar manner.
String jsonArray = gson.toJson(listOfDogs, typeListOfDogs);
GoalKicker.com – Android™ Notes for Professionals 1023
Section 219.2: Adding a custom Converter to Gson
Sometimes you need to serialize or deserialize some fields in a desired format, for example your backend may use
the format "YYYY-MM-dd HH:mm" for dates and you want your POJOS to use the DateTime class in Joda Time.
In order to automatically convert these strings into DateTimes object, you can use a custom converter.
/**
* Gson serialiser/deserialiser for converting Joda {@link DateTime} objects.
*/
public class DateTimeConverter implements JsonSerializer<DateTime>, JsonDeserializer<DateTime> {
private final DateTimeFormatter dateTimeFormatter;
@Inject
public DateTimeConverter() {
this.dateTimeFormatter = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm");
}
@Override
public JsonElement serialize(DateTime src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(dateTimeFormatter.print(src));
}
@Override
public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
if (json.getAsString() == null || json.getAsString().isEmpty()) {
return null;
}
return dateTimeFormatter.parseDateTime(json.getAsString());
}
}
To make Gson use the newly created converter you need to assign it when creating the Gson object:
DateTimeConverter dateTimeConverter = new DateTimeConverter();
Gson gson = new GsonBuilder().registerTypeAdapter(DateTime.class, dateTimeConverter)
.create();
String s = gson.toJson(DateTime.now());
// this will show the date in the desired format
In order to deserialize the date in that format you only have to define a field in the DateTime format:
public class SomePojo {
private DateTime someDate;
}
When Gson encounters a field of type DateTime, it will call your converter in order to deserialize the field.
Section 219.3: Parsing a List<String> with Gson
Method 1
Gson gson = new Gson();
GoalKicker.com – Android™ Notes for Professionals 1024
String json = "[ \"Adam\", \"John\", \"Mary\" ]";
Type type = new TypeToken<List<String>>(){}.getType();
List<String> members = gson.fromJson(json, type);
Log.v("Members", members.toString());
This is useful for most generic container classes, since you can't get the class of a parameterized type (ie: you can't
call List<String>.class).
Method 2
public class StringList extends ArrayList<String> { }
...
List<String> members = gson.fromJson(json, StringList.class);
Alternatively, you can always subclass the type you want, and then pass in that class. However this isn't always best
practice, since it will return to you an object of type StringList;
Section 219.4: Adding Gson to your project
dependencies {
compile 'com.google.code.gson:gson:2.8.1'
}
To use latest version of Gson
The below line will compile latest version of gson library every time you compile, you do not have to change version.
Pros: You can use latest features, speed and less bugs.
Cons: It might break compatibility with your code.
compile 'com.google.code.gson:gson:+'
Section 219.5: Parsing JSON to Generic Class Object with Gson
Suppose we have a JSON string
["first","second","third"]
We can parse this JSON string into a String array :
Gson gson = new Gson();
String jsonArray = "[\"first\",\"second\",\"third\"]";
String[] strings = gson.fromJson(jsonArray, String[].class);
But if we want parse it into a List<String> object, we must use TypeToken.
Here is the sample :
Gson gson = new Gson();
String jsonArray = "[\"first\",\"second\",\"third\"]";
List<String> stringList = gson.fromJson(jsonArray, new TypeToken<List<String>>() {}.getType());
GoalKicker.com – Android™ Notes for Professionals 1025
Suppose we have two classes below:
public class Outer<T> {
public int index;
public T data;
}
public class Person {
public String firstName;
public String lastName;
}
and we have a JSON string that should be parsed to a Outer<Person> object.
This example shows how to parse this JSON string to the related generic class object:
String json = "......";
Type userType = new TypeToken<Outer<Person>>(){}.getType();
Result<User> userResult = gson.fromJson(json,userType);
If the JSON string should be parsed to a Outer<List<Person>> object :
Type userListType = new TypeToken<Outer<List<Person>>>(){}.getType();
Result<List<User>> userListResult = gson.fromJson(json,userListType);
Section 219.6: Using Gson with inheritance
Gson does not support inheritance out of the box.
Let's say we have the following class hierarchy:
public class BaseClass {
int a;
public int getInt() {
return a;
}
}
public class DerivedClass1 extends BaseClass {
int b;
@Override
public int getInt() {
return b;
}
}
public class DerivedClass2 extends BaseClass {
int c;
@Override
public int getInt() {
return c;
}
}
And now we want to serialize an instance of DerivedClass1 to a JSON string
GoalKicker.com – Android™ Notes for Professionals 1026
DerivedClass1 derivedClass1 = new DerivedClass1();
derivedClass1.b = 5;
derivedClass1.a = 10;
Gson gson = new Gson();
String derivedClass1Json = gson.toJson(derivedClass1);
Now, in another place, we receive this json string and want to deserialize it - but in compile time we only know it is
supposed to be an instance of BaseClass:
BaseClass maybeDerivedClass1 = gson.fromJson(derivedClass1Json, BaseClass.class);
System.out.println(maybeDerivedClass1.getInt());
But GSON does not know derivedClass1Json was originally an instance of DerivedClass1, so this will print out 10.
How to solve this?
You need to build your own JsonDeserializer, that handles such cases. The solution is not perfectly clean, but I
could not come up with a better one.
First, add the following field to your base class
@SerializedName("type")
private String typeName;
And initialize it in the base class constructor
public BaseClass() {
typeName = getClass().getName();
}
Now add the following class:
public class JsonDeserializerWithInheritance<T> implements JsonDeserializer<T> {
@Override
public T deserialize(
JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
JsonPrimitive classNamePrimitive = (JsonPrimitive) jsonObject.get("type");
String className = classNamePrimitive.getAsString();
Class<?> clazz;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new JsonParseException(e.getMessage());
}
return context.deserialize(jsonObject, clazz);
}
}
All there is left to do is hook everything up -
GsonBuilder builder = new GsonBuilder();
builder
GoalKicker.com – Android™ Notes for Professionals 1027
.registerTypeAdapter(BaseClass.class, new JsonDeserializerWithInheritance<BaseClass>());
Gson gson = builder.create();
And now, running the following code-
DerivedClass1 derivedClass1 = new DerivedClass1();
derivedClass1.b = 5;
derivedClass1.a = 10;
String derivedClass1Json = gson.toJson(derivedClass1);
BaseClass maybeDerivedClass1 = gson.fromJson(derivedClass1Json, BaseClass.class);
System.out.println(maybeDerivedClass1.getInt());
Will print out 5.
Section 219.7: Parsing JSON property to enum with Gson
If you want to parse a String to enum with Gson:
{"status" : "open"}
public enum Status {
@SerializedName("open")
OPEN,
@SerializedName("waiting")
WAITING,
@SerializedName("confirm")
CONFIRM,
@SerializedName("ready")
READY
}
Section 219.8: Using Gson to load a JSON file from disk
This will load a JSON file from disk and convert it to the given type.
public static <T> T getFile(String fileName, Class<T> type) throws FileNotFoundException {
Gson gson = new GsonBuilder()
.create();
FileReader json = new FileReader(fileName);
return gson.fromJson(json, type);
}
Section 219.9: Using Gson as serializer with Retrofit
First of all you need to add the GsonConverterFactory to your build.gradle file
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
Then, you have to add the converter factory when creating the Retrofit Service:
Gson gson = new GsonBuilder().create();
new Retrofit.Builder()
.baseUrl(someUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
GoalKicker.com – Android™ Notes for Professionals 1028
.build()
.create(RetrofitService.class);
You can add custom converters when creating the Gson object that you are passing to the factory. Allowing you to
create custom type conversions.
Section 219.10: Parsing json array to generic class using Gson
Suppose we have a json
{
"total_count": 132,
"page_size": 2,
"page_index": 1,
"twitter_posts": [
{
"created_on": 1465935152,
"tweet_id": 210462857140252672,
"tweet": "Along with our new #Twitterbird, we've also updated our Display Guidelines",
"url": "https://twitter.com/twitterapi/status/210462857140252672"
},
{
"created_on": 1465995741,
"tweet_id": 735128881808691200,
"tweet": "Information on the upcoming changes to Tweets is now on the developer site",
"url": "https://twitter.com/twitterapi/status/735128881808691200"
}
]
}
We can parse this array into a Custom Tweets (tweets list container) object manually, but it is easier to do it with
fromJson method:
Gson gson = new Gson();
String jsonArray = "....";
Tweets tweets = gson.fromJson(jsonArray, Tweets.class);
Suppose we have two classes below:
class Tweets {
@SerializedName("total_count")
int totalCount;
@SerializedName("page_size")
int pageSize;
@SerializedName("page_index")
int pageIndex;
// all you need to do it is just define List variable with correct name
@SerializedName("twitter_posts")
List<Tweet> tweets;
}
class Tweet {
@SerializedName("created_on")
long createdOn;
@SerializedName("tweet_id")
String tweetId;
@SerializedName("tweet")
String tweetBody;
@SerializedName("url")
GoalKicker.com – Android™ Notes for Professionals 1029
String url;
}
and if you need just parse a json array you can use this code in your parsing:
String tweetsJsonArray = "[{.....},{.....}]"
List<Tweet> tweets = gson.fromJson(tweetsJsonArray, new TypeToken<List<Tweet>>() {}.getType());
Section 219.11: Custom JSON Deserializer using Gson
Imagine you have all dates in all responses in some custom format, for instance /Date(1465935152)/ and you want
apply general rule to deserialize all Json dates to java Date instances. In this case you need to implement custom
Json Deserializer.
Example of json:
{
"id": 1,
"created_on": "Date(1465935152)",
"updated_on": "Date(1465968945)",
"name": "Oleksandr"
}
Suppose we have this class below:
class User {
@SerializedName("id")
long id;
@SerializedName("created_on")
Date createdOn;
@SerializedName("updated_on")
Date updatedOn;
@SerializedName("name")
String name;
}
Custom deserializer:
class DateDeSerializer implements JsonDeserializer<Date> {
private static final String DATE_PREFIX = "/Date(";
private static final String DATE_SUFFIX = ")/";
@Override
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
String dateString = json.getAsString();
if (dateString.startsWith(DATE_PREFIX) && dateString.endsWith(DATE_SUFFIX)) {
dateString = dateString.substring(DATE_PREFIX.length(), dateString.length() -
DATE_SUFFIX.length());
} else {
throw new JsonParseException("Wrong date format: " + dateString);
}
return new Date(Long.parseLong(dateString) - TimeZone.getDefault().getRawOffset());
}
}
And the usage:
GoalKicker.com – Android™ Notes for Professionals 1030
Gson gson = new GsonBuilder()
.registerTypeAdapter(Date.class, new DateDeSerializer())
.create();
String json = "....";
User user = gson.fromJson(json, User.class);
Serialize and deserialize Jackson JSON strings with Date types
This also applies to the case where you want to make Gson Date conversion compatible with Jackson, for example.
Jackson usually serializes Date to "milliseconds since epoch" whereas Gson uses a readable format like Aug 31,
2016 10:26:17 to represent Date. This leads to JsonSyntaxExceptions in Gson when you try to deserialize a Jackson
format Date.
To circumvent this, you can add a custom serializer and a custom deserializer:
JsonSerializer<Date> ser = new JsonSerializer<Date>() {
@Override
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext
context) {
return src == null ? null : new JsonPrimitive(src.getTime());
}
};
JsonDeserializer<Date> deser = new JsonDeserializer<Date>() {
@Override
public Date deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
return json == null ? null : new Date(json.getAsLong());
}
};
Gson gson = new GsonBuilder()
.registerTypeAdapter(Date.class, ser)
.registerTypeAdapter(Date.class, deser)
.create();
Section 219.12: JSON Serialization/Deserialization with
AutoValue and Gson
Import in your gradle root file
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
Import in your gradle app file
apt 'com.google.auto.value:auto-value:1.2'
apt 'com.ryanharter.auto.value:auto-value-gson:0.3.1'
provided 'com.jakewharton.auto.value:auto-value-annotations:1.2-update1'
provided 'org.glassfish:javax.annotation:10.0-b28'
Create object with autovalue:
@AutoValue public abstract class SignIn {
@SerializedName("signin_token") public abstract String signinToken();
public abstract String username();
GoalKicker.com – Android™ Notes for Professionals 1031
public static TypeAdapter<SignIn> typeAdapter(Gson gson) {
return new AutoValue_SignIn.GsonTypeAdapter(gson);
}
public static SignIn create(String signin, String username) {
return new AutoValue_SignIn(signin, username);
}
}
Create your Gson converter with your GsonBuilder
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(
new AutoValueGsonTypeAdapterFactory())
.create());
Deserialize
String myJsonData = "{
\"signin_token\": \"mySigninToken\",
\"username\": \"myUsername\" }";
SignIn signInData = gson.fromJson(myJsonData, Signin.class);
Serialize
Signin myData = SignIn.create("myTokenData", "myUsername");
String myJsonData = gson.toJson(myData);
Using Gson is a great way to simplify Serialization and Deserialization code by using POJO objects. The side effect is
that reflection is costly performance wise. That's why using AutoValue-Gson to generate CustomTypeAdapter will
avoid this reflection cost while staying very simple to update when an api change is happening.
GoalKicker.com – Android™ Notes for Professionals 1032
Chapter 220: Android Architecture
Components
Android Architecture Components is new collection of libraries that help you design robust, testable, and
maintainable apps. Main parts are: Lifecycles, ViewModel, LiveData, Room.
Section 220.1: Using Lifecycle in AppCompatActivity
Extend your activity from this activity
public abstract class BaseCompatLifecycleActivity extends AppCompatActivity implements
LifecycleRegistryOwner {
// We need this class, because LifecycleActivity extends FragmentActivity not AppCompatActivity
@NonNull
private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
@NonNull
@Override
public LifecycleRegistry getLifecycle() {
return lifecycleRegistry;
}
}
Section 220.2: Add Architecture Components
Project build.gradle
allprojects {
repositories {
jcenter()
// Add this if you use Gradle 4.0+
google()
// Add this if you use Gradle < 4.0
maven { url 'https://maven.google.com' }
}
}
ext {
archVersion = '1.0.0-alpha5'
}
Application build gradle
// For Lifecycles, LiveData, and ViewModel
compile "android.arch.lifecycle:runtime:$archVersion"
compile "android.arch.lifecycle:extensions:$archVersion"
annotationProcessor "android.arch.lifecycle:compiler:$archVersion"
// For Room
compile "android.arch.persistence.room:runtime:$archVersion"
annotationProcessor "android.arch.persistence.room:compiler:$archVersion"
// For testing Room migrations
testCompile "android.arch.persistence.room:testing:$archVersion"
// For Room RxJava support
GoalKicker.com – Android™ Notes for Professionals 1033
compile "android.arch.persistence.room:rxjava2:$archVersion"
Section 220.3: ViewModel with LiveData transformations
public class BaseViewModel extends ViewModel {
private static final int TAG_SEGMENT_INDEX = 2;
private static final int VIDEOS_LIMIT = 100;
// We save input params here
private final MutableLiveData<Pair<String, String>> urlWithReferrerLiveData = new
MutableLiveData<>();
// transform specific uri param to "tag"
private final LiveData<String> currentTagLiveData =
Transformations.map(urlWithReferrerLiveData, pair -> {
Uri uri = Uri.parse(pair.first);
List<String> segments = uri.getPathSegments();
if (segments.size() > TAG_SEGMENT_INDEX)
return segments.get(TAG_SEGMENT_INDEX);
return null;
});
// transform "tag" to videos list
private final LiveData<List<VideoItem>> videoByTagData =
Transformations.switchMap(currentTagLiveData, tag -> contentRepository.getVideoByTag(tag,
VIDEOS_LIMIT));
ContentRepository contentRepository;
public BaseViewModel() {
// some inits
}
public void setUrlWithReferrer(String url, String referrer) {
// set value activates observers and transformations
urlWithReferrerLiveData.setValue(new Pair<>(url, referrer));
}
public LiveData<List<VideoItem>> getVideoByTagData() {
return videoByTagData;
}
}
Somewhere in UI:
public class VideoActivity extends BaseCompatLifecycleActivity {
private VideoViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get ViewModel
viewModel = ViewModelProviders.of(this).get(BaseViewModel.class);
// Add observer
viewModel.getVideoByTagData().observe(this, data -> {
// some checks
adapter.updateData(data);
});
GoalKicker.com – Android™ Notes for Professionals 1034
...
if (savedInstanceState == null) {
// init loading only at first creation
// you just set params and
viewModel.setUrlWithReferrer(url, referrer);
}
}
Section 220.4: Room peristence
Room require four parts: Database class, DAO classes, Entity classes and Migration classes (now you may use only
DDL methods):
Entity classes
// Set custom table name, add indexes
@Entity(tableName = "videos",
indices = {@Index("title")}
)
public final class VideoItem {
@PrimaryKey // required
public long articleId;
public String title;
public String url;
}
// Use ForeignKey for setup table relation
@Entity(tableName = "tags",
indices = {@Index("score"), @Index("videoId"), @Index("value")},
foreignKeys = @ForeignKey(entity = VideoItem.class,
parentColumns = "articleId",
childColumns = "videoId",
onDelete = ForeignKey.CASCADE)
)
public final class VideoTag {
@PrimaryKey
public long id;
public long videoId;
public String displayName;
public String value;
public double score;
}
DAO classes
@Dao
public interface VideoDao {
// Create insert with custom conflict strategy
@Insert(onConflict = OnConflictStrategy.REPLACE)
void saveVideos(List<VideoItem> videos);
// Simple update
@Update
void updateVideos(VideoItem... videos);
@Query("DELETE FROM tags WHERE videoId = :videoId")
void deleteTagsByVideoId(long videoId);
// Custom query, you may use select/delete here
@Query("SELECT v.* FROM tags t LEFT JOIN videos v ON v.articleId = t.videoId WHERE t.value =
GoalKicker.com – Android™ Notes for Professionals 1035
:tag ORDER BY updatedAt DESC LIMIT :limit")
LiveData<List<VideoItem>> getVideosByTag(String tag, int limit);
}
Database class
// register your entities and DAOs
@Database(entities = {VideoItem.class, VideoTag.class}, version = 2)
public abstract class ContentDatabase extends RoomDatabase {
public abstract VideoDao videoDao();
}
Migrations
public final class Migrations {
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
final String[] sqlQueries = {
"CREATE TABLE IF NOT EXISTS `tags` (`id` INTEGER PRIMARY KEY AUTOINCREMENT," +
" `videoId` INTEGER, `displayName` TEXT, `value` TEXT, `score` REAL," +
" FOREIGN KEY(`videoId`) REFERENCES `videos`(`articleId`)" +
" ON UPDATE NO ACTION ON DELETE CASCADE )",
"CREATE INDEX `index_tags_score` ON `tags` (`score`)",
"CREATE INDEX `index_tags_videoId` ON `tags` (`videoId`)"};
for (String query : sqlQueries) {
database.execSQL(query);
}
}
};
public static final Migration[] ALL = {MIGRATION_1_2};
private Migrations() {
}
}
Use in Application class or provide via Dagger
ContentDatabase provideContentDatabase() {
return Room.databaseBuilder(context, ContentDatabase.class, "data.db")
.addMigrations(Migrations.ALL).build();
}
Write your repository:
public final class ContentRepository {
private final ContentDatabase db;
private final VideoDao videoDao;
public ContentRepository(ContentDatabase contentDatabase, VideoDao videoDao) {
this.db = contentDatabase;
this.videoDao = videoDao;
}
public LiveData<List<VideoItem>> getVideoByTag(@Nullable String tag, int limit) {
// you may fetch from network, save to database
....
return videoDao.getVideosByTag(tag, limit);
}
GoalKicker.com – Android™ Notes for Professionals 1036
}
Use in ViewModel:
ContentRepository contentRepository = ...;
contentRepository.getVideoByTag(tag, limit);
Section 220.5: Custom LiveData
You may write custom LiveData, if you need custom logic.
Don't write custom class, if you only need to transform data (use Transformations class)
public class LocationLiveData extends LiveData<Location> {
private LocationManager locationManager;
private LocationListener listener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
setValue(location);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
// Do something
}
@Override
public void onProviderEnabled(String provider) {
// Do something
}
@Override
public void onProviderDisabled(String provider) {
// Do something
}
};
public LocationLiveData(Context context) {
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
}
@Override
protected void onActive() {
// We have observers, start working
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
}
@Override
protected void onInactive() {
// We have no observers, stop working
locationManager.removeUpdates(listener);
}
}
Section 220.6: Custom Lifecycle-aware component
Each UI component lifecycle changed as shown at image.
GoalKicker.com – Android™ Notes for Professionals 1037
You may create component, that will be notified on lifecycle state change:
public class MyLocationListener implements LifecycleObserver {
private boolean enabled = false;
private Lifecycle lifecycle;
public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
...
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void start() {
if (enabled) {
// connect
}
}
public void enable() {
enabled = true;
if (lifecycle.getState().isAtLeast(STARTED)) {
// connect if not connected
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void stop() {
// disconnect if connected
}
}
GoalKicker.com – Android™ Notes for Professionals 1038
Chapter 221: Jackson
Jackson is a multi-purpose Java library for processing JSON. Jackson aims to be the best possible combination of
fast, correct, lightweight, and ergonomic for developers.
Jackson features
Multi processing mode, and very good collaboration
Not only annotations, but also mixed annotations
Fully support generic types
Support polymorphic types
Section 221.1: Full Data Binding Example
JSON data
{
"name" : { "first" : "Joe", "last" : "Sixpack" },
"gender" : "MALE",
"verified" : false,
"userImage" : "keliuyue"
}
It takes two lines of Java to turn it into a User instance:
ObjectMapper mapper = new ObjectMapper(); // can reuse, share globally
User user = mapper.readValue(new File("user.json"), User.class);
User.class
public class User {
public enum Gender {MALE, FEMALE};
public static class Name {
private String _first, _last;
public String getFirst() {
return _first;
}
public String getLast() {
return _last;
}
public void setFirst(String s) {
_first = s;
}
public void setLast(String s) {
_last = s;
}
}
GoalKicker.com – Android™ Notes for Professionals 1039
private Gender _gender;
private Name _name;
private boolean _isVerified;
private byte[] _userImage;
public Name getName() {
return _name;
}
public boolean isVerified() {
return _isVerified;
}
public Gender getGender() {
return _gender;
}
public byte[] getUserImage() {
return _userImage;
}
public void setName(Name n) {
_name = n;
}
public void setVerified(boolean b) {
_isVerified = b;
}
public void setGender(Gender g) {
_gender = g;
}
public void setUserImage(byte[] b) {
_userImage = b;
}
}
Marshalling back to JSON is similarly straightforward:
mapper.writeValue(new File("user-modified.json"), user);
GoalKicker.com – Android™ Notes for Professionals 1040
Chapter 222: Smartcard
Section 222.1: Smart card send and receive
For connection, here is a snippet to help you understand:
//Allows you to enumerate and communicate with connected USB devices.
UsbManager mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
//Explicitly asking for permission
final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
PendingIntent mPermissionIntent = PendingIntent.getBroadcast(this, 0, new
Intent(ACTION_USB_PERMISSION), 0);
HashMap<String, UsbDevice> deviceList = mUsbManager.getDeviceList();
UsbDevice device = deviceList.get("//the device you want to work with");
if (device != null) {
mUsbManager.requestPermission(device, mPermissionIntent);
}
Now you have to understand that in java the communication takes place using package javax.smarcard which is not
available for Android so take a look here for getting an idea as to how you can communicate or send/receive APDU
(smartcard command).
Now as told in the answer mentioned above
You cannot simply send an APDU (smartcard command) over the bulk-out endpoint and expect to receive a
response APDU over the bulk-in endpoint. For getting the endpoints see the code snippet below :
UsbEndpoint epOut = null, epIn = null;
UsbInterface usbInterface;
UsbDeviceConnection connection = mUsbManager.openDevice(device);
for (int i = 0; i < device.getInterfaceCount(); i++) {
usbInterface = device.getInterface(i);
connection.claimInterface(usbInterface, true);
for (int j = 0; j < usbInterface.getEndpointCount(); j++) {
UsbEndpoint ep = usbInterface.getEndpoint(j);
if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
if (ep.getDirection() == UsbConstants.USB_DIR_OUT) {
// from host to device
epOut = ep;
} else if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
// from device to host
epIn = ep;
}
}
}
}
Now you have the bulk-in and bulk-out endpoints to send and receive APDU command and APDU response blocks:
For sending commands, see the code snippet below:
public void write(UsbDeviceConnection connection, UsbEndpoint epOut, byte[] command) {
GoalKicker.com – Android™ Notes for Professionals 1041
result = new StringBuilder();
connection.bulkTransfer(epOut, command, command.length, TIMEOUT);
//For Printing logs you can use result variable
for (byte bb : command) {
result.append(String.format(" %02X ", bb));
}
}
And for receive/ read a response see the code snippet below :
public int read(UsbDeviceConnection connection, UsbEndpoint epIn) {
result = new StringBuilder();
final byte[] buffer = new byte[epIn.getMaxPacketSize()];
int byteCount = 0;
byteCount = connection.bulkTransfer(epIn, buffer, buffer.length, TIMEOUT);
//For Printing logs you can use result variable
if (byteCount >= 0) {
for (byte bb : buffer) {
result.append(String.format(" %02X ", bb));
}
//Buffer received was : result.toString()
} else {
//Something went wrong as count was : " + byteCount
}
return byteCount;
}
Now if you see this answer here the 1st command to be sent is :
PC_to_RDR_IccPowerOn command to activate the card.
which you can create by reading section 6.1.1 of the USB Device Class Specifications doc here.
Now let's take an example of this command like the one here: 62000000000000000000 How you can send this is :
write(connection, epOut, "62000000000000000000");
Now after you have successfully sent the APDU command, you can read the response using :
read(connection, epIn);
And receive something like
80 18000000 00 00 00 00 00 3BBF11008131FE45455041000000000000000000000000F1
Now the response received in the code here will be in the result variable of read() method from code
GoalKicker.com – Android™ Notes for Professionals 1042
Chapter 223: Security
Section 223.1: Verifying App Signature - Tamper Detection
This technique details how to ensure that your .apk has been signed with your developer certificate, and leverages
the fact that the certificate remains consistent and that only you have access to it. We can break this technique into
3 simple steps:
Find your developer certificate signature.
Embed your signature in a String constant in your app.
Check that the signature at runtime matches our embedded developer signature.
Here's the code snippet:
private static final int VALID = 0;
private static final int INVALID = 1;
public static int checkAppSignature(Context context) {
try {
PackageInfo packageInfo =
context.getPackageManager().getPackageInfo(context.getPackageName(),
PackageManager.GET_SIGNATURES);
for (Signature signature : packageInfo.signatures) {
byte[] signatureBytes = signature.toByteArray();
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(signature.toByteArray());
final String currentSignature = Base64.encodeToString(md.digest(), Base64.DEFAULT);
Log.d("REMOVE_ME", "Include this string as a value for SIGNATURE:" + currentSignature);
//compare signatures
if (SIGNATURE.equals(currentSignature)){
return VALID;
};
}
} catch (Exception e) {
//assumes an issue in checking signature., but we let the caller decide on what to do.
}
return INVALID;
}
GoalKicker.com – Android™ Notes for Professionals 1043
Chapter 224: How to store passwords
securely
Section 224.1: Using AES for salted password encryption
This examples uses the AES algorithm for encrypting passwords. The salt length can be up to We are using the SecureRandom class to generate a salt, which is combined with the password to generate a secret
key. The classes used are already existing in Android packages javax.crypto and java.security.
Once a key is generated, we have to preserve this key in a variable or store it. We are storing it among the shared
preferences in the value S_KEY. Then, a password is encrypted using the doFinal method of the Cipher class once it
is initialised in ENCRYPT_MODE. Next, the encrypted password is converted from a byte array into a string and stored
among the shared preferences. The key used to generate an encrypted password can be used to decrypt the
password in a similar way:
public class MainActivity extends AppCompatActivity {
public static final String PROVIDER = "BC";
public static final int SALT_LENGTH = 20;
public static final int IV_LENGTH = 16;
public static final int PBE_ITERATION_COUNT = 100;
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
private static final String HASH_ALGORITHM = "SHA-512";
private static final String PBE_ALGORITHM = "PBEWithSHA256And256BitAES-CBC-BC";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
public static final String SECRET_KEY_ALGORITHM = "AES";
private static final String TAG = "EncryptionPassword";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String originalPassword = "ThisIsAndroidStudio%$";
Log.e(TAG, "originalPassword => " + originalPassword);
String encryptedPassword = encryptAndStorePassword(originalPassword);
Log.e(TAG, "encryptedPassword => " + encryptedPassword);
String decryptedPassword = decryptAndGetPassword();
Log.e(TAG, "decryptedPassword => " + decryptedPassword);
}
private String decryptAndGetPassword() {
SharedPreferences prefs = getSharedPreferences("pswd", MODE_PRIVATE);
String encryptedPasswrd = prefs.getString("token", "");
String passwrd = "";
if (encryptedPasswrd!=null && !encryptedPasswrd.isEmpty()) {
try {
String output = prefs.getString("S_KEY", "");
byte[] encoded = hexStringToByteArray(output);
SecretKey aesKey = new SecretKeySpec(encoded, SECRET_KEY_ALGORITHM);
passwrd = decrypt(aesKey, encryptedPasswrd);
} catch (Exception e) {
e.printStackTrace();
}
}
return passwrd;
}
public String encryptAndStorePassword(String password) {
GoalKicker.com – Android™ Notes for Professionals 1044
SharedPreferences.Editor editor = getSharedPreferences("pswd", MODE_PRIVATE).edit();
String encryptedPassword = "";
if (password!=null && !password.isEmpty()) {
SecretKey secretKey = null;
try {
secretKey = getSecretKey(password, generateSalt());
byte[] encoded = secretKey.getEncoded();
String input = byteArrayToHexString(encoded);
editor.putString("S_KEY", input);
encryptedPassword = encrypt(secretKey, password);
} catch (Exception e) {
e.printStackTrace();
}
editor.putString("token", encryptedPassword);
editor.commit();
}
return encryptedPassword;
}
public static String encrypt(SecretKey secret, String cleartext) throws Exception {
try {
byte[] iv = generateIv();
String ivHex = byteArrayToHexString(iv);
IvParameterSpec ivspec = new IvParameterSpec(iv);
Cipher encryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
encryptionCipher.init(Cipher.ENCRYPT_MODE, secret, ivspec);
byte[] encryptedText = encryptionCipher.doFinal(cleartext.getBytes("UTF-8"));
String encryptedHex = byteArrayToHexString(encryptedText);
return ivHex + encryptedHex;
} catch (Exception e) {
Log.e("SecurityException", e.getCause().getLocalizedMessage());
throw new Exception("Unable to encrypt", e);
}
}
public static String decrypt(SecretKey secret, String encrypted) throws Exception {
try {
Cipher decryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
String ivHex = encrypted.substring(0, IV_LENGTH * 2);
String encryptedHex = encrypted.substring(IV_LENGTH * 2);
IvParameterSpec ivspec = new IvParameterSpec(hexStringToByteArray(ivHex));
decryptionCipher.init(Cipher.DECRYPT_MODE, secret, ivspec);
byte[] decryptedText = decryptionCipher.doFinal(hexStringToByteArray(encryptedHex));
String decrypted = new String(decryptedText, "UTF-8");
return decrypted;
} catch (Exception e) {
Log.e("SecurityException", e.getCause().getLocalizedMessage());
throw new Exception("Unable to decrypt", e);
}
}
public static String generateSalt() throws Exception {
try {
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
byte[] salt = new byte[SALT_LENGTH];
random.nextBytes(salt);
String saltHex = byteArrayToHexString(salt);
return saltHex;
GoalKicker.com – Android™ Notes for Professionals 1045
} catch (Exception e) {
throw new Exception("Unable to generate salt", e);
}
}
public static String byteArrayToHexString(byte[] b) {
StringBuffer sb = new StringBuffer(b.length * 2);
for (int i = 0; i < b.length; i++) {
int v = b[i] & 0xff;
if (v < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v));
}
return sb.toString().toUpperCase();
}
public static byte[] hexStringToByteArray(String s) {
byte[] b = new byte[s.length() / 2];
for (int i = 0; i < b.length; i++) {
int index = i * 2;
int v = Integer.parseInt(s.substring(index, index + 2), 16);
b[i] = (byte) v;
}
return b;
}
public static SecretKey getSecretKey(String password, String salt) throws Exception {
try {
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(),
hexStringToByteArray(salt), PBE_ITERATION_COUNT, 256);
SecretKeyFactory factory = SecretKeyFactory.getInstance(PBE_ALGORITHM, PROVIDER);
SecretKey tmp = factory.generateSecret(pbeKeySpec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), SECRET_KEY_ALGORITHM);
return secret;
} catch (Exception e) {
throw new Exception("Unable to get secret key", e);
}
}
private static byte[] generateIv() throws NoSuchAlgorithmException, NoSuchProviderException {
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
byte[] iv = new byte[IV_LENGTH];
random.nextBytes(iv);
return iv;
}
}
GoalKicker.com – Android™ Notes for Professionals 1046
Chapter 225: Secure SharedPreferences
Parameter Definition
input String value to encrypt or decrypt.
Shared Preferences are key-value based XML files. It is located under
/data/data/package_name/shared_prefs/<filename.xml>.
So a user with root privileges can navigate to this location and can change its values. If you want to protect values in
your shared preferences, you can write a simple encryption and decryption mechanism.
You should know tough, that Shared Preferences were never built to be secure, it's just a simple way to persist data.
Section 225.1: Securing a Shared Preference
Simple Codec
Here to illustrate the working principle we can use simple encryption and decryption as follows.
public static String encrypt(String input) {
// Simple encryption, not very strong!
return Base64.encodeToString(input.getBytes(), Base64.DEFAULT);
}
public static String decrypt(String input) {
return new String(Base64.decode(input, Base64.DEFAULT));
}
Implementation Technique
public static String pref_name = "My_Shared_Pref";
// To Write
SharedPreferences preferences = getSharedPreferences(pref_name, MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString(encrypt("password"), encrypt("my_dummy_pass"));
editor.apply(); // Or commit if targeting old devices
// To Read
SharedPreferences preferences = getSharedPreferences(pref_name, MODE_PRIVATE);
String passEncrypted = preferences.getString(encrypt("password"), encrypt("default_value"));
String password = decrypt(passEncrypted);
GoalKicker.com – Android™ Notes for Professionals 1047
Chapter 226: Secure SharedPreferences
Parameter Definition
input String value to encrypt or decrypt.
Shared Preferences are key-value based XML files. It is located under
/data/data/package_name/shared_prefs/<filename.xml>.
So a user with root privileges can navigate to this location and can change its values. If you want to protect values in
your shared preferences, you can write a simple encryption and decryption mechanism.
You should know tough, that Shared Preferences were never built to be secure, it's just a simple way to persist data.
Section 226.1: Securing a Shared Preference
Simple Codec
Here to illustrate the working principle we can use simple encryption and decryption as follows.
public static String encrypt(String input) {
// Simple encryption, not very strong!
return Base64.encodeToString(input.getBytes(), Base64.DEFAULT);
}
public static String decrypt(String input) {
return new String(Base64.decode(input, Base64.DEFAULT));
}
Implementation Technique
public static String pref_name = "My_Shared_Pref";
// To Write
SharedPreferences preferences = getSharedPreferences(pref_name, MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString(encrypt("password"), encrypt("my_dummy_pass"));
editor.apply(); // Or commit if targeting old devices
// To Read
SharedPreferences preferences = getSharedPreferences(pref_name, MODE_PRIVATE);
String passEncrypted = preferences.getString(encrypt("password"), encrypt("default_value"));
String password = decrypt(passEncrypted);
GoalKicker.com – Android™ Notes for Professionals 1048
Chapter 227: SQLite
SQLite is a relational database management system written in C. To begin working with SQLite databases within the
Android framework, define a class that extends SQLiteOpenHelper, and customize as needed.
Section 227.1: onUpgrade() method
SQLiteOpenHelper is a helper class to manage database creation and version management.
In this class, the onUpgrade() method is responsible for upgrading the database when you make changes to the
schema. It is called when the database file already exists, but its version is lower than the one specified in the
current version of the app. For each database version, the specific changes you made have to be applied.
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Loop through each version when an upgrade occurs.
for (int version = oldVersion + 1; version <= newVersion; version++) {
switch (version) {
case 2:
// Apply changes made in version 2
db.execSQL(
"ALTER TABLE " +
TABLE_PRODUCTS +
" ADD COLUMN " +
COLUMN_DESCRIPTION +
" TEXT;"
);
break;
case 3:
// Apply changes made in version 3
db.execSQL(CREATE_TABLE_TRANSACTION);
break;
}
}
}
Section 227.2: Reading data from a Cursor
Here is an example of a method that would live inside a SQLiteOpenHelper subclass. It uses the searchTerm String
to filter the results, iterates through the Cursor's contents, and returns those contents in a List of Product Objects.
First, define the Product POJO class that will be the container for each row retrieved from the database:
public class Product {
long mId;
String mName;
String mDescription;
float mValue;
public Product(long id, String name, String description, float value) {
mId = id;
mName = name;
mDescription = description;
mValue = value;
}
}
GoalKicker.com – Android™ Notes for Professionals 1049
Then, define the method that will query the database, and return a List of Product Objects:
public List<Product> searchForProducts(String searchTerm) {
// When reading data one should always just get a readable database.
final SQLiteDatabase database = this.getReadableDatabase();
final Cursor cursor = database.query(
// Name of the table to read from
TABLE_NAME,
// String array of the columns which are supposed to be read
new String[]{COLUMN_NAME, COLUMN_DESCRIPTION, COLUMN_VALUE},
// The selection argument which specifies which row is read.
// ? symbols are parameters.
COLUMN_NAME + " LIKE ?",
// The actual parameters values for the selection as a String array.
// ? above take the value from here
new String[]{"%" + searchTerm + "%"},
// GroupBy clause. Specify a column name to group similar values
// in that column together.
null,
// Having clause. When using the GroupBy clause this allows you to
// specify which groups to include.
null,
// OrderBy clause. Specify a column name here to order the results
// according to that column. Optionally append ASC or DESC to specify
// an ascending or descending order.
null
);
// To increase performance first get the index of each column in the cursor
final int idIndex = cursor.getColumnIndex(COLUMN_ID);
final int nameIndex = cursor.getColumnIndex(COLUMN_NAME);
final int descriptionIndex = cursor.getColumnIndex(COLUMN_DESCRIPTION);
final int valueIndex = cursor.getColumnIndex(COLUMN_VALUE);
try {
// If moveToFirst() returns false then cursor is empty
if (!cursor.moveToFirst()) {
return new ArrayList<>();
}
final List<Product> products = new ArrayList<>();
do {
// Read the values of a row in the table using the indexes acquired above
final long id = cursor.getLong(idIndex);
final String name = cursor.getString(nameIndex);
final String description = cursor.getString(descriptionIndex);
final float value = cursor.getFloat(valueIndex);
products.add(new Product(id, name, description, value));
} while (cursor.moveToNext());
GoalKicker.com – Android™ Notes for Professionals 1050
return products;
} finally {
// Don't forget to close the Cursor once you are done to avoid memory leaks.
// Using a try/finally like in this example is usually the best way to handle this
cursor.close();
// close the database
database.close();
}
}
Section 227.3: Using the SQLiteOpenHelper class
public class DatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "Example.db";
private static final int DATABASE_VERSION = 3;
// For all Primary Keys _id should be used as column name
public static final String COLUMN_ID = "_id";
// Definition of table and column names of Products table
public static final String TABLE_PRODUCTS = "Products";
public static final String COLUMN_NAME = "Name";
public static final String COLUMN_DESCRIPTION = "Description";
public static final String COLUMN_VALUE = "Value";
// Definition of table and column names of Transactions table
public static final String TABLE_TRANSACTIONS = "Transactions";
public static final String COLUMN_PRODUCT_ID = "ProductId";
public static final String COLUMN_AMOUNT = "Amount";
// Create Statement for Products Table
private static final String CREATE_TABLE_PRODUCT = "CREATE TABLE " + TABLE_PRODUCTS + " (" +
COLUMN_ID + " INTEGER PRIMARY KEY, " +
COLUMN_DESCRIPTION + " TEXT, " +
COLUMN_NAME + " TEXT, " +
COLUMN_VALUE + " REAL" +
");";
// Create Statement for Transactions Table
private static final String CREATE_TABLE_TRANSACTION = "CREATE TABLE " + TABLE_TRANSACTIONS + "
(" +
COLUMN_ID + " INTEGER PRIMARY KEY," +
COLUMN_PRODUCT_ID + " INTEGER," +
COLUMN_AMOUNT + " INTEGER," +
" FOREIGN KEY (" + COLUMN_PRODUCT_ID + ") REFERENCES " + TABLE_PRODUCTS + "(" +
COLUMN_ID + ")" +
");";
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// onCreate should always create your most up to date database
// This method is called when the app is newly installed
db.execSQL(CREATE_TABLE_PRODUCT);
db.execSQL(CREATE_TABLE_TRANSACTION);
}
GoalKicker.com – Android™ Notes for Professionals 1051
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// onUpgrade is responsible for upgrading the database when you make
// changes to the schema. For each version the specific changes you made
// in that version have to be applied.
for (int version = oldVersion + 1; version <= newVersion; version++) {
switch (version) {
case 2:
db.execSQL("ALTER TABLE " + TABLE_PRODUCTS + " ADD COLUMN " +
COLUMN_DESCRIPTION + " TEXT;");
break;
case 3:
db.execSQL(CREATE_TABLE_TRANSACTION);
break;
}
}
}
}
Section 227.4: Insert data into database
// You need a writable database to insert data
final SQLiteDatabase database = openHelper.getWritableDatabase();
// Create a ContentValues instance which contains the data for each column
// You do not need to specify a value for the PRIMARY KEY column.
// Unique values for these are automatically generated.
final ContentValues values = new ContentValues();
values.put(COLUMN_NAME, model.getName());
values.put(COLUMN_DESCRIPTION, model.getDescription());
values.put(COLUMN_VALUE, model.getValue());
// This call performs the update
// The return value is the rowId or primary key value for the new row!
// If this method returns -1 then the insert has failed.
final int id = database.insert(
TABLE_NAME, // The table name in which the data will be inserted
null, // String: optional; may be null. If your provided values is empty,
// no column names are known and an empty row can't be inserted.
// If not set to null, this parameter provides the name
// of nullable column name to explicitly insert a NULL
values // The ContentValues instance which contains the data
);
Section 227.5: Bulk insert
Here is an example of inserting large chunks of data at once. All the data you want to insert is gathered inside of a
ContentValues array.
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
int count = 0;
String table = null;
int uriType = IChatContract.MessageColumns.uriMatcher.match(uri);
switch (uriType) {
case IChatContract.MessageColumns.MESSAGES:
GoalKicker.com – Android™ Notes for Professionals 1052
table = IChatContract.MessageColumns.TABLE_NAME;
break;
}
mDatabase.beginTransaction();
try {
for (ContentValues cv : values) {
long rowID = mDatabase.insert(table, " ", cv);
if (rowID <= 0) {
throw new SQLException("Failed to insert row into " + uri);
}
}
mDatabase.setTransactionSuccessful();
getContext().getContentResolver().notifyChange(uri, null);
count = values.length;
} finally {
mDatabase.endTransaction();
}
return count;
}
And here is an example of how to use it:
ContentResolver resolver = mContext.getContentResolver();
ContentValues[] valueList = new ContentValues[object.size()];
//add whatever you like to the valueList
resolver.bulkInsert(IChatContract.MessageColumns.CONTENT_URI, valueList);
Section 227.6: Create a Contract, Helper and Provider for
SQLite in Android
DBContract.java
//Define the tables and columns of your local database
public final class DBContract {
/*Content Authority its a name for the content provider, is convenient to use the package app name to
be unique on the device */
public static final String CONTENT_AUTHORITY = "com.yourdomain.yourapp";
//Use CONTENT_AUTHORITY to create all the database URI's that the app will use to link the
content provider.
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
/*the name of the uri that can be the same as the name of your table.
this will translate to content://com.yourdomain.yourapp/user/ as a valid URI
*/
public static final String PATH_USER = "User";
// To prevent someone from accidentally instantiating the contract class,
// give it an empty constructor.
public DBContract () {}
//Intern class that defines the user table
public static final class UserEntry implements BaseColumns {
public static final URI CONTENT_URI =
BASE_CONTENT_URI.buildUpon().appendPath(PATH_USER).build();
public static final String CONTENT_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE+"/"+CONTENT_AUTHORITY+"/"+PATH_USER;
GoalKicker.com – Android™ Notes for Professionals 1053
//Name of the table
public static final String TABLE_NAME="User";
//Columns of the user table
public static final String COLUMN_Name="Name";
public static final String COLUMN_Password="Password";
public static Uri buildUri(long id){
return ContentUris.withAppendedId(CONTENT_URI,id);
}
}
DBHelper.java
public class DBHelper extends SQLiteOpenHelper{
//if you change the schema of the database, you must increment this number
private static final int DATABASE_VERSION=1;
static final String DATABASE_NAME="mydatabase.db";
private static DBHelper mInstance=null;
public static DBHelper getInstance(Context ctx){
if(mInstance==null){
mInstance= new DBHelper(ctx.getApplicationContext());
}
return mInstance;
}
public DBHelper(Context context){
super(context,DATABASE_NAME,null,DATABASE_VERSION);
}
public int GetDatabase_Version() {
return DATABASE_VERSION;
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase){
//Create the table users
final String SQL_CREATE_TABLE_USERS="CREATE TABLE "+UserEntry.TABLE_NAME+ " ("+
UserEntry._ID+" INTEGER PRIMARY KEY, "+
UserEntry.COLUMN_Name+" TEXT , "+
UserEntry.COLUMN_Password+" TEXT "+
" ); ";
sqLiteDatabase.execSQL(SQL_CREATE_TABLE_USERS);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + UserEntry.TABLE_NAME);
}
}
DBProvider.java
public class DBProvider extends ContentProvider {
private static final UriMatcher sUriMatcher = buildUriMatcher();
private DBHelper mDBHelper;
GoalKicker.com – Android™ Notes for Professionals 1054
private Context mContext;
static final int USER = 100;
static UriMatcher buildUriMatcher() {
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
final String authority = DBContract.CONTENT_AUTHORITY;
matcher.addURI(authority, DBContract.PATH_USER, USER);
return matcher;
}
@Override
public boolean onCreate() {
mDBHelper = new DBHelper(getContext());
return false;
}
public PeaberryProvider(Context context) {
mDBHelper = DBHelper.getInstance(context);
mContext = context;
}
@Override
public String getType(Uri uri) {
// determine what type of Uri is
final int match = sUriMatcher.match(uri);
switch (match) {
case USER:
return DBContract.UserEntry.CONTENT_TYPE;
default:
throw new UnsupportedOperationException("Uri unknown: " + uri);
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor retCursor;
try {
switch (sUriMatcher.match(uri)) {
case USER: {
retCursor = mDBHelper.getReadableDatabase().query(
DBContract.UserEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
);
break;
}
default:
throw new UnsupportedOperationException("Uri unknown: " + uri);
}
} catch (Exception ex) {
Log.e("Cursor", ex.toString());
GoalKicker.com – Android™ Notes for Professionals 1055
} finally {
mDBHelper.close();
}
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = mDBHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
Uri returnUri;
try {
switch (match) {
case USER: {
long _id = db.insert(DBContract.UserEntry.TABLE_NAME, null, values);
if (_id > 0)
returnUri = DBContract.UserEntry.buildUri(_id);
else
throw new android.database.SQLException("Error at inserting row in " +
uri);
break;
}
default:
throw new UnsupportedOperationException("Uri unknown: " + uri);
}
mContext.getContentResolver().notifyChange(uri, null);
return returnUri;
} catch (Exception ex) {
Log.e("Insert", ex.toString());
db.close();
} finally {
db.close();
}
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final SQLiteDatabase db = DBHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
int deletedRows;
if (null == selection) selection = "1";
try {
switch (match) {
case USER:
deletedRows = db.delete(
DBContract.UserEntry.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new UnsupportedOperationException("Uri unknown: " + uri);
}
if (deletedRows != 0) {
mContext.getContentResolver().notifyChange(uri, null);
}
return deletedRows;
} catch (Exception ex) {
Log.e("Insert", ex.toString());
} finally {
db.close();
}
return 0;
GoalKicker.com – Android™ Notes for Professionals 1056
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final SQLiteDatabase db = mDBHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
int updatedRows;
try {
switch (match) {
case USER:
updatedRows = db.update(DBContract.UserEntry.TABLE_NAME, values, selection,
selectionArgs);
break;
default:
throw new UnsupportedOperationException("Uri unknown: " + uri);
}
if (updatedRows != 0) {
mContext.getContentResolver().notifyChange(uri, null);
}
return updatedRows;
} catch (Exception ex) {
Log.e("Update", ex.toString());
} finally {
db.close();
}
return -1;
}
}
How to Use:
public void InsertUser() {
try {
ContentValues userValues = getUserData("Jhon","XXXXX");
DBProvider dbProvider = new DBProvider(mContext);
dbProvider.insert(UserEntry.CONTENT_URI, userValues);
} catch (Exception ex) {
Log.e("Insert", ex.toString());
}
}
public ContentValues getUserData(String name, String pass) {
ContentValues userValues = new ContentValues();
userValues.put(UserEntry.COLUMN_Name, name);
userValues.put(UserEntry.COLUMN_Password, pass);
return userValues;
}
Section 227.7: Delete row(s) from the table
To delete all rows from the table
//get writable database
SQLiteDatabase db = openHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
db.close();
GoalKicker.com – Android™ Notes for Professionals 1057
To delete all rows from the table and get the count of the deleted row in return value
//get writable database
SQLiteDatabase db = openHelper.getWritableDatabase();
int numRowsDeleted = db.delete(TABLE_NAME, String.valueOf(1), null);
db.close();
To delete row(s) with WHERE condition
//get writable database
SQLiteDatabase db = openHelper.getWritableDatabase();
String whereClause = KEY_NAME + " = ?";
String[] whereArgs = new String[]{String.valueOf(KEY_VALUE)};
//for multiple condition, join them with AND
//String whereClause = KEY_NAME1 + " = ? AND " + KEY_NAME2 + " = ?";
//String[] whereArgs = new String[]{String.valueOf(KEY_VALUE1), String.valueOf(KEY_VALUE2)};
int numRowsDeleted = db.delete(TABLE_NAME, whereClause, whereArgs);
db.close();
Section 227.8: Updating a row in a table
// You need a writable database to update a row
final SQLiteDatabase database = openHelper.getWritableDatabase();
// Create a ContentValues instance which contains the up to date data for each column
// Unlike when inserting data you need to specify the value for the PRIMARY KEY column as well
final ContentValues values = new ContentValues();
values.put(COLUMN_ID, model.getId());
values.put(COLUMN_NAME, model.getName());
values.put(COLUMN_DESCRIPTION, model.getDescription());
values.put(COLUMN_VALUE, model.getValue());
// This call performs the update
// The return value tells you how many rows have been updated.
final int count = database.update(
TABLE_NAME, // The table name in which the data will be updated
values, // The ContentValues instance with the new data
COLUMN_ID + " = ?", // The selection which specifies which row is updated. ? symbols are
parameters.
new String[] { // The actual parameters for the selection as a String[].
String.valueOf(model.getId())
}
);
Section 227.9: Performing a Transaction
Transactions can be used to make multiple changes to the database atomically. Any normal transaction follows this
pattern:
// You need a writable database to perform transactions
final SQLiteDatabase database = openHelper.getWritableDatabase();
// This call starts a transaction
database.beginTransaction();
GoalKicker.com – Android™ Notes for Professionals 1058
// Using try/finally is essential to reliably end transactions even
// if exceptions or other problems occur.
try {
// Here you can make modifications to the database
database.insert(TABLE_CARS, null, productValues);
database.update(TABLE_BUILDINGS, buildingValues, COLUMN_ID + " = ?", new String[] {
String.valueOf(buildingId) });
// This call marks a transaction as successful.
// This causes the changes to be written to the database once the transaction ends.
database.setTransactionSuccessful();
} finally {
// This call ends a transaction.
// If setTransactionSuccessful() has not been called then all changes
// will be rolled back and the database will not be modified.
database.endTransaction();
}
Calling beginTransaction() inside of an active transactions has no effect.
Section 227.10: Create Database from assets folder
Put your dbname.sqlite or dbname.db file in assets folder of your project.
public class Databasehelper extends SQLiteOpenHelper {
public static final String TAG = Databasehelper.class.getSimpleName();
public static int flag;
// Exact Name of you db file that you put in assets folder with extension.
static String DB_NAME = "dbname.sqlite";
private final Context myContext;
String outFileName = "";
private String DB_PATH;
private SQLiteDatabase db;
public Databasehelper(Context context) {
super(context, DB_NAME, null, 1);
this.myContext = context;
ContextWrapper cw = new ContextWrapper(context);
DB_PATH = cw.getFilesDir().getAbsolutePath() + "/databases/";
Log.e(TAG, "Databasehelper: DB_PATH " + DB_PATH);
outFileName = DB_PATH + DB_NAME;
File file = new File(DB_PATH);
Log.e(TAG, "Databasehelper: " + file.exists());
if (!file.exists()) {
file.mkdir();
}
}
/**
* Creates a empty database on the system and rewrites it with your own database.
*/
public void createDataBase() throws IOException {
boolean dbExist = checkDataBase();
if (dbExist) {
//do nothing - database already exist
} else {
//By calling this method and empty database will be created into the default system
path
//of your application so we are gonna be able to overwrite that database with our
database.
GoalKicker.com – Android™ Notes for Professionals 1059
this.getReadableDatabase();
try {
copyDataBase();
} catch (IOException e) {
throw new Error("Error copying database");
}
}
}
/**
* Check if the database already exist to avoid re-copying the file each time you open the
application.
*
* @return true if it exists, false if it doesn't
*/
private boolean checkDataBase() {
SQLiteDatabase checkDB = null;
try {
checkDB = SQLiteDatabase.openDatabase(outFileName, null,
SQLiteDatabase.OPEN_READWRITE);
} catch (SQLiteException e) {
try {
copyDataBase();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (checkDB != null) {
checkDB.close();
}
return checkDB != null ? true : false;
}
/**
* Copies your database from your local assets-folder to the just created empty database in
the
* system folder, from where it can be accessed and handled.
* This is done by transfering bytestream.
*/
private void copyDataBase() throws IOException {
Log.i("Database",
"New database is being copied to device!");
byte[] buffer = new byte[1024];
OutputStream myOutput = null;
int length;
// Open your local db as the input stream
InputStream myInput = null;
try {
myInput = myContext.getAssets().open(DB_NAME);
// transfer bytes from the inputfile to the
// outputfile
myOutput = new FileOutputStream(DB_PATH + DB_NAME);
while ((length = myInput.read(buffer)) > 0) {
myOutput.write(buffer, 0, length);
}
myOutput.close();
myOutput.flush();
myInput.close();
Log.i("Database",
GoalKicker.com – Android™ Notes for Professionals 1060
"New database has been copied to device!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void openDataBase() throws SQLException {
//Open the database
String myPath = DB_PATH + DB_NAME;
db = SQLiteDatabase.openDatabase(myPath, null, SQLiteDatabase.OPEN_READWRITE);
Log.e(TAG, "openDataBase: Open " + db.isOpen());
}
@Override
public synchronized void close() {
if (db != null)
db.close();
super.close();
}
public void onCreate(SQLiteDatabase arg0) {
}
@Override
public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) {
}
}
Here is How you can access database object to your activity.
// Create Databasehelper class object in your activity.
private Databasehelper db;
Then in onCreate Method initialize it and call createDatabase() Method as show below.
db = new Databasehelper(MainActivity.this);
try {
db.createDataBase();
} catch (Exception e) {
e.printStackTrace();
}
Perform all of your insert, update, delete and select operation as shown below.
String query = "select Max(Id) as Id from " + TABLE_NAME;
db.openDataBase();
int count = db.getId(query);
db.close();
Section 227.11: Store image into SQLite
Setting Up the database
public class DatabaseHelper extends SQLiteOpenHelper {
// Database Version
private static final int DATABASE_VERSION = 1;
GoalKicker.com – Android™ Notes for Professionals 1061
// Database Name
private static final String DATABASE_NAME = "database_name";
// Table Names
private static final String DB_TABLE = "table_image";
// column names
private static final String KEY_NAME = "image_name";
private static final String KEY_IMAGE = "image_data";
// Table create statement
private static final String CREATE_TABLE_IMAGE = "CREATE TABLE " + DB_TABLE + "("+
KEY_NAME + " TEXT," +
KEY_IMAGE + " BLOB);";
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// creating table
db.execSQL(CREATE_TABLE_IMAGE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// on upgrade drop older tables
db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
// create new table
onCreate(db);
}
}
Insert in the Database:
public void addEntry( String name, byte[] image) throws SQLiteException{
SQLiteDatabase database = this.getWritableDatabase();
ContentValues cv = new ContentValues();
cv.put(KEY_NAME, name);
cv.put(KEY_IMAGE, image);
database.insert( DB_TABLE, null, cv );
}
Retrieving data:
byte[] image = cursor.getBlob(1);
Note:
1. Before inserting into database, you need to convert your Bitmap image into byte array first then apply it
using database query.
2. When retrieving from database, you certainly have a byte array of image, what you need to do is to convert
byte array back to original image. So, you have to make use of BitmapFactory to decode.
Below is an Utility class which I hope could help you:
GoalKicker.com – Android™ Notes for Professionals 1062
public class DbBitmapUtility {
// convert from bitmap to byte array
public static byte[] getBytes(Bitmap bitmap) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(CompressFormat.PNG, 0, stream);
return stream.toByteArray();
}
// convert from byte array to bitmap
public static Bitmap getImage(byte[] image) {
return BitmapFactory.decodeByteArray(image, 0, image.length);
}
}
Section 227.12: Exporting and importing a database
You might want to import and export your database for bacukups for example. Don't forget about the permissions.
public void exportDatabase(){
try
{
File sd = Environment.getExternalStorageDirectory();
File data = Environment.getDataDirectory();
String currentDBPath = "//data//MY.PACKAGE.NAME//databases//MY_DATABASE_NAME";
String backupDBPath = "MY_DATABASE_FILE.db";
File currentDB = new File(data, currentDBPath);
File backupDB = new File(sd, backupDBPath);
FileChannel src = new FileInputStream(currentDB).getChannel();
FileChannel dst = new FileOutputStream(backupDB).getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
Toast.makeText(c, c.getResources().getString(R.string.exporterenToast),
Toast.LENGTH_SHORT).show();
}
catch (Exception e) {
Toast.makeText(c, c.getResources().getString(R.string.portError),
Toast.LENGTH_SHORT).show();
Log.d("Main", e.toString());
}
}
public void importDatabase(){
try
{
File sd = Environment.getExternalStorageDirectory();
File data = Environment.getDataDirectory();
String currentDBPath = "//data//" + "MY.PACKAGE.NAME" + "//databases//" +
"MY_DATABASE_NAME";
String backupDBPath = "MY_DATABASE_FILE.db";
File backupDB = new File(data, currentDBPath);
File currentDB = new File(sd, backupDBPath);
FileChannel src = new FileInputStream(currentDB).getChannel();
FileChannel dst = new FileOutputStream(backupDB).getChannel();
dst.transferFrom(src, 0, src.size());
GoalKicker.com – Android™ Notes for Professionals 1063
src.close();
dst.close();
Toast.makeText(c, c.getResources().getString(R.string.importerenToast),
Toast.LENGTH_LONG).show();
}
catch (Exception e) {
Toast.makeText(c, c.getResources().getString(R.string.portError),
Toast.LENGTH_SHORT).show();
}
}
GoalKicker.com – Android™ Notes for Professionals 1064
Chapter 228: Accessing SQLite databases
using the ContentValues class
Section 228.1: Inserting and updating rows in a SQLite
database
First, you need to open your SQLite database, which can be done as follows:
SQLiteDatabase myDataBase;
String mPath = dbhelper.DATABASE_PATH + dbhelper.DATABASE_NAME;
myDataBase = SQLiteDatabase.openDatabase(mPath, null, SQLiteDatabase.OPEN_READWRITE);
After opening the database, you can easily insert or update rows by using the ContentValues class. The following
examples assume that a first name is given by str_edtfname and a last nameby str_edtlname. You also need to
replace table_name by the name of your table that you want to modify.
Inserting data
ContentValues values = new ContentValues();
values.put("First_Name", str_edtfname);
values.put("Last_Name", str_edtlname);
myDataBase.insert("table_name", null, values);
Updating data
ContentValues values = new ContentValues();
values.put("First_Name", str_edtfname);
values.put("Last_Name", str_edtlname);
myDataBase.update("table_name", values, "id" + " = ?", new String[] {id});
GoalKicker.com – Android™ Notes for Professionals 1065
Chapter 229: Firebase
Firebase is a mobile and web application platform with tools and infrastructure designed to help developers build
high-quality apps.
Features
Firebase Cloud Messaging, Firebase Auth, Realtime Database, Firebase Storage, Firebase Hosting, Firebase Test Lab
for Android, Firebase Crash Reporting.
Section 229.1: Add Firebase to Your Android Project
Here are simplified steps (based on the official documentation) required to create a Firebase project and connect it
with an Android app.
Add Firebase to your app
1. Create a Firebase project in the Firebase console and click Create New Project.
2. Click Add Firebase to your Android app and follow the setup steps.
3. When prompted, enter your app's package name.
It's important to enter the fully qualified package name your app is using; this can only be set when you add
an app to your Firebase project.
4. At the end, you'll download a google-services.json file. You can download this file again at any time.
5. If you haven't done so already, copy the google-services.json file into your project's module folder,
typically app/.
The next step is to Add the SDK to integrate the Firebase libraries in the project.
Add the SDK
To integrate the Firebase libraries into one of your own projects, you need to perform a few basic tasks to prepare
your Android Studio project. You may have already done this as part of adding Firebase to your app.
1. Add rules to your root-level build.gradle file, to include the google-services plugin:
buildscript {
// ...
dependencies {
// ...
classpath 'com.google.gms:google-services:3.1.0'
}
}
Then, in your module Gradle file (usually the app/build.gradle), add the apply plugin line at the bottom of the file
to enable the Gradle plugin:
apply plugin: 'com.android.application'
android {
// ...
}
GoalKicker.com – Android™ Notes for Professionals 1066
dependencies {
// ...
compile 'com.google.firebase:firebase-core:11.0.4'
}
// ADD THIS AT THE BOTTOM
apply plugin: 'com.google.gms.google-services'
The final step is to add the dependencies for the Firebase SDK using one or more libraries available for the
different Firebase features.
Gradle Dependency Line Service
com.google.firebase:firebase-core:11.0.4 Analytics
com.google.firebase:firebase-database:11.0.4 Realtime Database
com.google.firebase:firebase-storage:11.0.4 Storage
com.google.firebase:firebase-crash:11.0.4 Crash Reporting
com.google.firebase:firebase-auth:11.0.4 Authentication
com.google.firebase:firebase-messaging:11.0.4 Cloud Messaging / Notifications
com.google.firebase:firebase-config:11.0.4 Remote Config
com.google.firebase:firebase-invites:11.0.4 Invites / Dynamic Links
com.google.firebase:firebase-ads:11.0.4 AdMob
com.google.android.gms:play-services-appindexing:11.0.4 App Indexing
Section 229.2: Updating a Firebase users's email
public class ChangeEmailActivity extends BaseAppCompatActivity implements
ReAuthenticateDialogFragment.OnReauthenticateSuccessListener {
@BindView(R.id.et_change_email)
EditText mEditText;
private FirebaseUser mFirebaseUser;
@OnClick(R.id.btn_change_email)
void onChangeEmailClick() {
FormValidationUtils.clearErrors(mEditText);
if (FormValidationUtils.isBlank(mEditText)) {
FormValidationUtils.setError(null, mEditText, "Please enter email");
return;
}
if (!FormValidationUtils.isEmailValid(mEditText)) {
FormValidationUtils.setError(null, mEditText, "Please enter valid email");
return;
}
changeEmail(mEditText.getText().toString());
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mFirebaseUser = mFirebaseAuth.getCurrentUser();
}
GoalKicker.com – Android™ Notes for Professionals 1067
private void changeEmail(String email) {
DialogUtils.showProgressDialog(this, "Changing Email", "Please wait...", false);
mFirebaseUser.updateEmail(email)
.addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
DialogUtils.dismissProgressDialog();
if (task.isSuccessful()) {
showToast("Email updated successfully.");
return;
}
if (task.getException() instanceof
FirebaseAuthRecentLoginRequiredException) {
FragmentManager fm = getSupportFragmentManager();
ReAuthenticateDialogFragment reAuthenticateDialogFragment = new
ReAuthenticateDialogFragment();
reAuthenticateDialogFragment.show(fm,
reAuthenticateDialogFragment.getClass().getSimpleName());
}
}
});
}
@Override
protected int getLayoutResourceId() {
return R.layout.activity_change_email;
}
@Override
public void onReauthenticateSuccess() {
changeEmail(mEditText.getText().toString());
}
}
Section 229.3: Create a Firebase user
public class SignUpActivity extends BaseAppCompatActivity {
@BindView(R.id.tIETSignUpEmail)
EditText mEditEmail;
@BindView(R.id.tIETSignUpPassword)
EditText mEditPassword;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@OnClick(R.id.btnSignUpSignUp)
void signUp() {
FormValidationUtils.clearErrors(mEditEmail, mEditPassword);
if (FormValidationUtils.isBlank(mEditEmail)) {
mEditEmail.setError("Please enter email");
return;
}
if (!FormValidationUtils.isEmailValid(mEditEmail)) {
GoalKicker.com – Android™ Notes for Professionals 1068
mEditEmail.setError("Please enter valid email");
return;
}
if (TextUtils.isEmpty(mEditPassword.getText())) {
mEditPassword.setError("Please enter password");
return;
}
createUserWithEmailAndPassword(mEditEmail.getText().toString(),
mEditPassword.getText().toString());
}
private void createUserWithEmailAndPassword(String email, String password) {
DialogUtils.showProgressDialog(this, "", getString(R.string.str_creating_account), false);
mFirebaseAuth
.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (!task.isSuccessful()) {
Toast.makeText(SignUpActivity.this, task.getException().getMessage(),
Toast.LENGTH_SHORT).show();
DialogUtils.dismissProgressDialog();
} else {
Toast.makeText(SignUpActivity.this,
R.string.str_registration_successful, Toast.LENGTH_SHORT).show();
DialogUtils.dismissProgressDialog();
startActivity(new Intent(SignUpActivity.this, HomeActivity.class));
}
}
});
}
@Override
protected int getLayoutResourceId() {
return R.layout.activity_sign_up;
}
}
Section 229.4: Change Password
public class ChangePasswordActivity extends BaseAppCompatActivity implements
ReAuthenticateDialogFragment.OnReauthenticateSuccessListener {
@BindView(R.id.et_change_password)
EditText mEditText;
private FirebaseUser mFirebaseUser;
@OnClick(R.id.btn_change_password)
void onChangePasswordClick() {
FormValidationUtils.clearErrors(mEditText);
if (FormValidationUtils.isBlank(mEditText)) {
FormValidationUtils.setError(null, mEditText, "Please enter password");
return;
}
changePassword(mEditText.getText().toString());
}
GoalKicker.com – Android™ Notes for Professionals 1069
private void changePassword(String password) {
DialogUtils.showProgressDialog(this, "Changing Password", "Please wait...", false);
mFirebaseUser.updatePassword(password)
.addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
DialogUtils.dismissProgressDialog();
if (task.isSuccessful()) {
showToast("Password updated successfully.");
return;
}
if (task.getException() instanceof
FirebaseAuthRecentLoginRequiredException) {
FragmentManager fm = getSupportFragmentManager();
ReAuthenticateDialogFragment reAuthenticateDialogFragment = new
ReAuthenticateDialogFragment();
reAuthenticateDialogFragment.show(fm,
reAuthenticateDialogFragment.getClass().getSimpleName());
}
}
});
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mFirebaseUser = mFirebaseAuth.getCurrentUser();
}
@Override
protected int getLayoutResourceId() {
return R.layout.activity_change_password;
}
@Override
public void onReauthenticateSuccess() {
changePassword(mEditText.getText().toString());
}
}
Section 229.5: Firebase Cloud Messaging
First of all you need to setup your project adding Firebase to your Android project following the steps described in
this topic.
Set up Firebase and the FCM SDK
Add the FCM dependency to your app-level build.gradle file
dependencies {
compile 'com.google.firebase:firebase-messaging:11.0.4'
}
And at the very bottom (this is important) add:
// ADD THIS AT THE BOTTOM
apply plugin: 'com.google.gms.google-services'
GoalKicker.com – Android™ Notes for Professionals 1070
Edit your app manifest
Add the following to your app's manifest:
A service that extends FirebaseMessagingService. This is required if you want to do any message handling
beyond receiving notifications on apps in the background.
A service that extends FirebaseInstanceIdService to handle the creation, rotation, and updating of
registration tokens.
For example:
<service
android:name=".MyInstanceIdListenerService">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
</intent-filter>
</service>
<service
android:name=".MyFcmListenerService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Here are simple implementations of the 2 services.
To retrieve the current registration token extend the FirebaseInstanceIdService class and override the
onTokenRefresh() method:
public class MyInstanceIdListenerService extends FirebaseInstanceIdService {
// Called if InstanceID token is updated. Occurs if the security of the previous token had been
// compromised. This call is initiated by the InstanceID provider.
@Override
public void onTokenRefresh() {
// Get updated InstanceID token.
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
// Send this token to your server or store it locally
}
}
To receive messages, use a service that extends FirebaseMessagingService and override the onMessageReceived
method.
public class MyFcmListenerService extends FirebaseMessagingService {
/**
* Called when message is received.
*
* @param remoteMessage Object representing the message received from Firebase Cloud Messaging.
*/
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
String from = remoteMessage.getFrom();
// Check if message contains a data payload.
if (remoteMessage.getData().size() > 0) {
GoalKicker.com – Android™ Notes for Professionals 1071
Log.d(TAG, "Message data payload: " + remoteMessage.getData());
Map<String, String> data = remoteMessage.getData();
}
// Check if message contains a notification payload.
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
}
// do whatever you want with this, post your own notification, or update local state
}
in Firebase can grouped user by their behavior like "AppVersion,free user,purchase user,or any specific rules" and
then send notification to specific group by send Topic Feature in fireBase.
to register user in topic use
FirebaseMessaging.getInstance().subscribeToTopic("Free");
then in fireBase console, send notification by topic name
More info in the dedicated topic Firebase Cloud Messaging.
Section 229.6: Firebase Storage Operations
With this example, you will be able to perform following operations:
1. Connect to Firebase Storage
2. Create a directory named “images”
3. Upload a file in images directory
4. Download a file from images directory
5. Delete a file from images directory
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CODE_PICK_IMAGE = 1;
private static final int PERMISSION_READ_WRITE_EXTERNAL_STORAGE = 2;
private FirebaseStorage mFirebaseStorage;
private StorageReference mStorageReference;
private StorageReference mStorageReferenceImages;
private Uri mUri;
private ImageView mImageView;
private ProgressDialog mProgressDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
mImageView = (ImageView) findViewById(R.id.imageView);
setSupportActionBar(toolbar);
// Create an instance of Firebase Storage
mFirebaseStorage = FirebaseStorage.getInstance();
}
private void pickImage() {
Intent intent = new Intent(Intent.ACTION_PICK,
GoalKicker.com – Android™ Notes for Professionals 1072
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_CODE_PICK_IMAGE) {
String filePath = FileUtil.getPath(this, data.getData());
mUri = Uri.fromFile(new File(filePath));
uploadFile(mUri);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull
int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_READ_WRITE_EXTERNAL_STORAGE) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
pickImage();
}
}
}
private void showProgressDialog(String title, String message) {
if (mProgressDialog != null && mProgressDialog.isShowing())
mProgressDialog.setMessage(message);
else
mProgressDialog = ProgressDialog.show(this, title, message, true, false);
}
private void hideProgressDialog() {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
}
private void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
public void showHorizontalProgressDialog(String title, String body) {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.setTitle(title);
mProgressDialog.setMessage(body);
} else {
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setTitle(title);
mProgressDialog.setMessage(body);
mProgressDialog.setIndeterminate(false);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setProgress(0);
mProgressDialog.setMax(100);
mProgressDialog.setCancelable(false);
mProgressDialog.show();
}
}
GoalKicker.com – Android™ Notes for Professionals 1073
public void updateProgress(int progress) {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.setProgress(progress);
}
}
/**
* Step 1: Create a Storage
*
* @param view
*/
public void onCreateReferenceClick(View view) {
mStorageReference = mFirebaseStorage.getReferenceFromUrl("gs://**something**.appspot.com");
showToast("Reference Created Successfully.");
findViewById(R.id.button_step_2).setEnabled(true);
}
/**
* Step 2: Create a directory named "Images"
*
* @param view
*/
public void onCreateDirectoryClick(View view) {
mStorageReferenceImages = mStorageReference.child("images");
showToast("Directory 'images' created Successfully.");
findViewById(R.id.button_step_3).setEnabled(true);
}
/**
* Step 3: Upload an Image File and display it on ImageView
*
* @param view
*/
public void onUploadFileClick(View view) {
if (ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSION_READ_WRITE_EXTERNAL_STORAGE);
else {
pickImage();
}
}
/**
* Step 4: Download an Image File and display it on ImageView
*
* @param view
*/
public void onDownloadFileClick(View view) {
downloadFile(mUri);
}
/**
* Step 5: Delete am Image File and remove Image from ImageView
*
* @param view
*/
public void onDeleteFileClick(View view) {
deleteFile(mUri);
GoalKicker.com – Android™ Notes for Professionals 1074
}
private void showAlertDialog(Context ctx, String title, String body,
DialogInterface.OnClickListener okListener) {
if (okListener == null) {
okListener = new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
};
}
AlertDialog.Builder builder = new
AlertDialog.Builder(ctx).setMessage(body).setPositiveButton("OK", okListener).setCancelable(false);
if (!TextUtils.isEmpty(title)) {
builder.setTitle(title);
}
builder.show();
}
private void uploadFile(Uri uri) {
mImageView.setImageResource(R.drawable.placeholder_image);
StorageReference uploadStorageReference =
mStorageReferenceImages.child(uri.getLastPathSegment());
final UploadTask uploadTask = uploadStorageReference.putFile(uri);
showHorizontalProgressDialog("Uploading", "Please wait...");
uploadTask
.addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() {
@Override
public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {
hideProgressDialog();
Uri downloadUrl = taskSnapshot.getDownloadUrl();
Log.d("MainActivity", downloadUrl.toString());
showAlertDialog(MainActivity.this, "Upload Complete",
downloadUrl.toString(), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
findViewById(R.id.button_step_3).setEnabled(false);
findViewById(R.id.button_step_4).setEnabled(true);
}
});
Glide.with(MainActivity.this)
.load(downloadUrl)
.into(mImageView);
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception exception) {
exception.printStackTrace();
// Handle unsuccessful uploads
hideProgressDialog();
}
})
.addOnProgressListener(MainActivity.this, new
OnProgressListener<UploadTask.TaskSnapshot>() {
GoalKicker.com – Android™ Notes for Professionals 1075
@Override
public void onProgress(UploadTask.TaskSnapshot taskSnapshot) {
int progress = (int) (100 * (float) taskSnapshot.getBytesTransferred() /
taskSnapshot.getTotalByteCount());
Log.i("Progress", progress + "");
updateProgress(progress);
}
});
}
private void downloadFile(Uri uri) {
mImageView.setImageResource(R.drawable.placeholder_image);
final StorageReference storageReferenceImage =
mStorageReferenceImages.child(uri.getLastPathSegment());
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "Firebase Storage");
if (!mediaStorageDir.exists()) {
if (!mediaStorageDir.mkdirs()) {
Log.d("MainActivity", "failed to create Firebase Storage directory");
}
}
final File localFile = new File(mediaStorageDir, uri.getLastPathSegment());
try {
localFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
showHorizontalProgressDialog("Downloading", "Please wait...");
storageReferenceImage.getFile(localFile).addOnSuccessListener(new
OnSuccessListener<FileDownloadTask.TaskSnapshot>() {
@Override
public void onSuccess(FileDownloadTask.TaskSnapshot taskSnapshot) {
hideProgressDialog();
showAlertDialog(MainActivity.this, "Download Complete",
localFile.getAbsolutePath(), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
findViewById(R.id.button_step_4).setEnabled(false);
findViewById(R.id.button_step_5).setEnabled(true);
}
});
Glide.with(MainActivity.this)
.load(localFile)
.into(mImageView);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception exception) {
// Handle any errors
hideProgressDialog();
exception.printStackTrace();
}
}).addOnProgressListener(new OnProgressListener<FileDownloadTask.TaskSnapshot>() {
@Override
public void onProgress(FileDownloadTask.TaskSnapshot taskSnapshot) {
int progress = (int) (100 * (float) taskSnapshot.getBytesTransferred() /
taskSnapshot.getTotalByteCount());
Log.i("Progress", progress + "");
updateProgress(progress);
GoalKicker.com – Android™ Notes for Professionals 1076
}
});
}
private void deleteFile(Uri uri) {
showProgressDialog("Deleting", "Please wait...");
StorageReference storageReferenceImage =
mStorageReferenceImages.child(uri.getLastPathSegment());
storageReferenceImage.delete().addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
hideProgressDialog();
showAlertDialog(MainActivity.this, "Success", "File deleted successfully.", new
DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
mImageView.setImageResource(R.drawable.placeholder_image);
findViewById(R.id.button_step_3).setEnabled(true);
findViewById(R.id.button_step_4).setEnabled(false);
findViewById(R.id.button_step_5).setEnabled(false);
}
});
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "Firebase Storage");
if (!mediaStorageDir.exists()) {
if (!mediaStorageDir.mkdirs()) {
Log.d("MainActivity", "failed to create Firebase Storage directory");
}
}
deleteFiles(mediaStorageDir);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception exception) {
hideProgressDialog();
exception.printStackTrace();
}
});
}
private void deleteFiles(File directory) {
if (directory.isDirectory())
for (File child : directory.listFiles())
child.delete();
}
}
By default, Firebase Storage rules applies Authentication restriction. If user is authenticated, only then, he can
perform operations on Firebase Storage, else he cannot. I have disabled the authentication part in this demo by
updating Storage rules. Previously, rules were looking like:
service firebase.storage {
match /b/**something**.appspot.com/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
But I changed to skip the authentication:
GoalKicker.com – Android™ Notes for Professionals 1077
service firebase.storage {
match /b/**something**.appspot.com/o {
match /{allPaths=**} {
allow read, write;
}
}
}
Section 229.7: Firebase Realtime Database: how to set/get
data
Note: Let's setup some anonymous authentication for the example
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
Once it is done, create a child by editing your database address. For example:
https://your-project.firebaseio.com/ to https://your-project.firebaseio.com/chat
We will put data to this location from our Android device. You don't have to create the database structure (tabs,
fields... etc), it will be automatically created when you'll send Java object to Firebase!
Create a Java object that contains all the attributes you want to send to the database:
public class ChatMessage {
private String username;
private String message;
public ChatMessage(String username, String message) {
this.username = username;
this.message = message;
}
public ChatMessage() {} // you MUST have an empty constructor
public String getUsername() {
return username;
}
public String getMessage() {
return message;
}
}
Then in your activity:
if (FirebaseAuth.getInstance().getCurrentUser() == null) {
FirebaseAuth.getInstance().signInAnonymously().addOnCompleteListener(new
OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isComplete() && task.isSuccessful()){
FirebaseDatabase database = FirebaseDatabase.getInstance();
GoalKicker.com – Android™ Notes for Professionals 1078
DatabaseReference reference = database.getReference("chat"); // reference is
'chat' because we created the database at /chat
}
}
});
}
To send a value:
ChatMessage msg = new ChatMessage("user1", "Hello World!");
reference.push().setValue(msg);
To receive changes that occurs in the database:
reference.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
ChatMessage msg = dataSnapshot.getValue(ChatMessage.class);
Log.d(TAG, msg.getUsername()+" "+msg.getMessage());
}
public void onChildChanged(DataSnapshot dataSnapshot, String s) {}
public void onChildRemoved(DataSnapshot dataSnapshot) {}
public void onChildMoved(DataSnapshot dataSnapshot, String s) {}
public void onCancelled(DatabaseError databaseError) {}
});
Section 229.8: Demo of FCM based notifications
This example shows how to use the Firebase Cloud Messaging(FCM) platform. FCM is a successor of Google Cloud
Messaging(GCM). It does not require C2D_MESSAGE permissions from the app users.
Steps to integrate FCM are as follows.
1. Create sample hello world project in Android Studio Your Android studio screen would look like the following
picture.
GoalKicker.com – Android™ Notes for Professionals 1079
2. Next step is to set up firebase project. Visit https://console.firebase.google.com and create a project with an
identical name, so that you can track it easily.
GoalKicker.com – Android™ Notes for Professionals 1080
3. Now it is time to add firebase to your sample android project you have just created. You will need package
name of your project and Debug signing certificate SHA-1(optional).
a. Package name - It can be found from the android manifest XML file.
b. Debug signing SHA-1 certificate - It can be found by running following command in the terminal.
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -
keypass android
Enter this information in the firebase console and add the app to firebase project. Once you click on add app
button, your browser would automatically download a JSON file named "google-services.json".
4. Now copy the google-services.json file you have just downloaded into your Android app module root
directory.
GoalKicker.com – Android™ Notes for Professionals 1081
5. Follow the instructions given on the firebase console as you proceed ahead. a. Add following code line to
GoalKicker.com – Android™ Notes for Professionals 1082
your project level build.gradle
dependencies{ classpath 'com.google.gms:google-services:3.1.0' .....
b. Add following code line at the end of your app level build.gradle.
//following are the dependencies to be added
compile 'com.google.firebase:firebase-messaging:11.0.4'
compile 'com.android.support:multidex:1.0.1'
}
// this line goes to the end of the file
apply plugin: 'com.google.gms.google-services'
c. Android studio would ask you to sync project. Click on Sync now.
6. Next task is to add two services. a. One extending FirebaseMessagingService with intent-filter as following
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
b. One extending FirebaseInstanceIDService.
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
</intent-filter>
7. FirebaseMessagingService code should look like this.
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import com.google.firebase.messaging.FirebaseMessagingService;
public class MyFirebaseMessagingService extends FirebaseMessagingService {
public MyFirebaseMessagingService() {
}
}
8. FirebaseInstanceIdService should look like this.
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import com.google.firebase.iid.FirebaseInstanceIdService;
public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {
public MyFirebaseInstanceIDService() {
}
}
9. Now it is time to capture the device registration token. Add following line of code to MainActivity's onCreate
method.
GoalKicker.com – Android™ Notes for Professionals 1083
String token = FirebaseInstanceId.getInstance().getToken();
Log.d("FCMAPP", "Token is "+token);
10. Once we have the access token, we can use firebase console to send out the notification. Run the app on
GoalKicker.com – Android™ Notes for Professionals 1084
GoalKicker.com – Android™ Notes for Professionals 1085
GoalKicker.com – Android™ Notes for Professionals 1086
your android handset.
GoalKicker.com – Android™ Notes for Professionals 1087
GoalKicker.com – Android™ Notes for Professionals 1088
Click on Notification in Firebase console and UI will help you to send out your first message. Firebase offers
functionality to send messages to single device(By using the device token id we captured) or all the users using our
app or to specific group of users. Once you send your first message, your mobile screen should look like following.
Thank you
Section 229.9: Sign In Firebase user with email and password
public class LoginActivity extends BaseAppCompatActivity {
@BindView(R.id.tIETLoginEmail)
EditText mEditEmail;
@BindView(R.id.tIETLoginPassword)
EditText mEditPassword;
@Override
protected void onResume() {
super.onResume();
FirebaseUser firebaseUser = mFirebaseAuth.getCurrentUser();
if (firebaseUser != null)
startActivity(new Intent(this, HomeActivity.class));
}
@Override
protected int getLayoutResourceId() {
return R.layout.activity_login;
}
@OnClick(R.id.btnLoginLogin)
void onSignInClick() {
FormValidationUtils.clearErrors(mEditEmail, mEditPassword);
if (FormValidationUtils.isBlank(mEditEmail)) {
FormValidationUtils.setError(null, mEditEmail, "Please enter email");
return;
}
GoalKicker.com – Android™ Notes for Professionals 1089
if (!FormValidationUtils.isEmailValid(mEditEmail)) {
FormValidationUtils.setError(null, mEditEmail, "Please enter valid email");
return;
}
if (TextUtils.isEmpty(mEditPassword.getText())) {
FormValidationUtils.setError(null, mEditPassword, "Please enter password");
return;
}
signInWithEmailAndPassword(mEditEmail.getText().toString(),
mEditPassword.getText().toString());
}
private void signInWithEmailAndPassword(String email, String password) {
DialogUtils.showProgressDialog(this, "", getString(R.string.sign_in), false);
mFirebaseAuth
.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
DialogUtils.dismissProgressDialog();
if (task.isSuccessful()) {
Toast.makeText(LoginActivity.this, "Login Successful",
Toast.LENGTH_SHORT).show();
startActivity(new Intent(LoginActivity.this, HomeActivity.class));
finish();
} else {
Toast.makeText(LoginActivity.this, task.getException().getMessage(),
Toast.LENGTH_SHORT).show();
}
}
});
}
@OnClick(R.id.btnLoginSignUp)
void onSignUpClick() {
startActivity(new Intent(this, SignUpActivity.class));
}
@OnClick(R.id.btnLoginForgotPassword)
void forgotPassword() {
startActivity(new Intent(this, ForgotPasswordActivity.class));
}
}
Section 229.10: Send Firebase password reset email
public class ForgotPasswordActivity extends AppCompatActivity {
@BindView(R.id.tIETForgotPasswordEmail)
EditText mEditEmail;
private FirebaseAuth mFirebaseAuth;
private FirebaseAuth.AuthStateListener mAuthStateListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GoalKicker.com – Android™ Notes for Professionals 1090
setContentView(R.layout.activity_forgot_password);
ButterKnife.bind(this);
mFirebaseAuth = FirebaseAuth.getInstance();
mAuthStateListener = new FirebaseAuth.AuthStateListener() {
@Override
public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
FirebaseUser firebaseUser = firebaseAuth.getCurrentUser();
if (firebaseUser != null) {
// Do whatever you want with the UserId by firebaseUser.getUid()
} else {
}
}
};
}
@Override
protected void onStart() {
super.onStart();
mFirebaseAuth.addAuthStateListener(mAuthStateListener);
}
@Override
protected void onStop() {
super.onStop();
if (mAuthStateListener != null) {
mFirebaseAuth.removeAuthStateListener(mAuthStateListener);
}
}
@OnClick(R.id.btnForgotPasswordSubmit)
void onSubmitClick() {
if (FormValidationUtils.isBlank(mEditEmail)) {
FormValidationUtils.setError(null, mEditEmail, "Please enter email");
return;
}
if (!FormValidationUtils.isEmailValid(mEditEmail)) {
FormValidationUtils.setError(null, mEditEmail, "Please enter valid email");
return;
}
DialogUtils.showProgressDialog(this, "", "Please wait...", false);
mFirebaseAuth.sendPasswordResetEmail(mEditEmail.getText().toString())
.addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
DialogUtils.dismissProgressDialog();
if (task.isSuccessful()) {
Toast.makeText(ForgotPasswordActivity.this, "An email has been sent to
you.", Toast.LENGTH_SHORT).show();
finish();
} else {
Toast.makeText(ForgotPasswordActivity.this,
task.getException().getMessage(), Toast.LENGTH_SHORT).show();
}
}
});
}
GoalKicker.com – Android™ Notes for Professionals 1091
}
Section 229.11: Re-Authenticate Firebase user
public class ReAuthenticateDialogFragment extends DialogFragment {
@BindView(R.id.et_dialog_reauthenticate_email)
EditText mEditTextEmail;
@BindView(R.id.et_dialog_reauthenticate_password)
EditText mEditTextPassword;
private OnReauthenticateSuccessListener mOnReauthenticateSuccessListener;
@OnClick(R.id.btn_dialog_reauthenticate)
void onReauthenticateClick() {
FormValidationUtils.clearErrors(mEditTextEmail, mEditTextPassword);
if (FormValidationUtils.isBlank(mEditTextEmail)) {
FormValidationUtils.setError(null, mEditTextEmail, "Please enter email");
return;
}
if (!FormValidationUtils.isEmailValid(mEditTextEmail)) {
FormValidationUtils.setError(null, mEditTextEmail, "Please enter valid email");
return;
}
if (TextUtils.isEmpty(mEditTextPassword.getText())) {
FormValidationUtils.setError(null, mEditTextPassword, "Please enter password");
return;
}
reauthenticateUser(mEditTextEmail.getText().toString(),
mEditTextPassword.getText().toString());
}
private void reauthenticateUser(String email, String password) {
DialogUtils.showProgressDialog(getActivity(), "Re-Authenticating", "Please wait...",
false);
FirebaseUser firebaseUser = FirebaseAuth.getInstance().getCurrentUser();
AuthCredential authCredential = EmailAuthProvider.getCredential(email, password);
firebaseUser.reauthenticate(authCredential)
.addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
DialogUtils.dismissProgressDialog();
if (task.isSuccessful()) {
mOnReauthenticateSuccessListener.onReauthenticateSuccess();
dismiss();
} else {
((BaseAppCompatActivity)
getActivity()).showToast(task.getException().getMessage());
}
}
});
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mOnReauthenticateSuccessListener = (OnReauthenticateSuccessListener) context;
GoalKicker.com – Android™ Notes for Professionals 1092
}
@OnClick(R.id.btn_dialog_reauthenticate_cancel)
void onCancelClick() {
dismiss();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_reauthenticate, container);
ButterKnife.bind(this, view);
return view;
}
@Override
public void onResume() {
super.onResume();
Window window = getDialog().getWindow();
window.setLayout(WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT);
}
interface OnReauthenticateSuccessListener {
void onReauthenticateSuccess();
}
}
Section 229.12: Firebase Sign Out
Initialization of variable
private GoogleApiClient mGoogleApiClient;
You must have to Write this Code in onCreate() method of all that when u put signout button.
mGoogleApiClient = new GoogleApiClient.Builder(this)
.enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */)
.addApi(Auth.GOOGLE_SIGN_IN_API)
.build();
Put below code on signout button.
Auth.GoogleSignInApi.signOut(mGoogleApiClient).setResultCallback(
new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
FirebaseAuth.getInstance().signOut();
Intent i1 = new Intent(MainActivity.this, GoogleSignInActivity.class);
startActivity(i1);
Toast.makeText(MainActivity.this, "Logout Successfully!",
Toast.LENGTH_SHORT).show();
}
});
GoalKicker.com – Android™ Notes for Professionals 1093
Chapter 230: Firebase Cloud Messaging
Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably deliver messages at no
cost.
Using FCM, you can notify a client app that new email or other data is available to sync. You can send notification
messages to drive user reengagement and retention. For use cases such as instant messaging, a message can
transfer a payload of up to 4KB to a client app.
Section 230.1: Set Up a Firebase Cloud Messaging Client App
on Android
1. Complete the Installation and setup part to connect your app to Firebase.
This will create the project in Firebase.
2. Add the dependency for Firebase Cloud Messaging to your module-level build.gradle file:
dependencies {
compile 'com.google.firebase:firebase-messaging:10.2.1'
}
Now you are ready to work with the FCM in Android.
FCM clients require devices running Android 2.3 or higher that also have the Google Play Store app installed, or an
emulator running Android 2.3 with Google APIs.
Edit your AndroidManifest.xml file
<service
android:name=".MyFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
<service
android:name=".MyFirebaseInstanceIDService">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
</intent-filter>
</service>
Section 230.2: Receive Messages
To receive messages, use a service that extends FirebaseMessagingService and override the onMessageReceived
method.
public class MyFcmListenerService extends FirebaseMessagingService {
/**
* Called when message is received.
*
* @param remoteMessage Object representing the message received from Firebase Cloud Messaging.
*/
@Override
public void onMessageReceived(RemoteMessage message) {
GoalKicker.com – Android™ Notes for Professionals 1094
String from = message.getFrom();
// Check if message contains a data payload.
if (remoteMessage.getData().size() > 0) {
Log.d(TAG, "Message data payload: " + remoteMessage.getData());
Map<String, String> data = message.getData();
}
// Check if message contains a notification payload.
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
}
//.....
}
When the app is in the background, Android directs notification messages to the system tray. A user tap on the
notification opens the app launcher by default.
This includes messages that contain both notification and data payload (and all messages sent from the
Notifications console). In these cases, the notification is delivered to the device's system tray, and the data payload
is delivered in the extras of the intent of your launcher Activity.
Here a short recap:
App state Notification Data Both
Foreground onMessageReceived onMessageReceived onMessageReceived
Background System tray onMessageReceived Notification: system tray
Data: in extras of the intent.
Section 230.3: This code that i have implemnted in my app for
pushing image,message and also link for opening in your
webView
This is my FirebaseMessagingService
public class MyFirebaseMessagingService extends FirebaseMessagingService {
Bitmap bitmap;
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
String message = remoteMessage.getData().get("message");
//imageUri will contain URL of the image to be displayed with Notification
String imageUri = remoteMessage.getData().get("image");
String link=remoteMessage.getData().get("link");
//To get a Bitmap image from the URL received
bitmap = getBitmapfromUrl(imageUri);
sendNotification(message, bitmap,link);
}
/**
* Create and show a simple notification containing the received FCM message.
*/
private void sendNotification(String messageBody, Bitmap image, String link) {
Intent intent = new Intent(this, NewsListActivity.class);
GoalKicker.com – Android™ Notes for Professionals 1095
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra("LINK",link);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
PendingIntent.FLAG_ONE_SHOT);
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
.setLargeIcon(image)/*Notification icon image*/
.setSmallIcon(R.drawable.hindi)
.setContentTitle(messageBody)
.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(image))/*Notification with Image*/
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
}
public Bitmap getBitmapfromUrl(String imageUrl) {
try {
URL url = new URL(imageUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
Bitmap bitmap = BitmapFactory.decodeStream(input);
return bitmap;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}}
And this is MainActivity to open link in my WebView or other browser depand on your requirement through intents.
if (getIntent().getExtras() != null) {
if (getIntent().getStringExtra("LINK")!=null) {
Intent i=new Intent(this,BrowserActivity.class);
i.putExtra("link",getIntent().getStringExtra("LINK"));
i.putExtra("PUSH","yes");
NewsListActivity.this.startActivity(i);
finish();
}}
Section 230.4: Registration token
On initial startup of your app, the FCM SDK generates a registration token for the client app instance.
If you want to target single devices or create device groups, you'll need to access this token by extending
FirebaseInstanceIdService.
The onTokenRefresh callback fires whenever a new token is generated and you can use the method
FirebaseInstanceID.getToken() to retrieve the current token.
Example:
GoalKicker.com – Android™ Notes for Professionals 1096
public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {
/**
* Called if InstanceID token is updated. This may occur if the security of
* the previous token had been compromised. Note that this is called when the InstanceID token
* is initially generated so this is where you would retrieve the token.
*/
@Override
public void onTokenRefresh() {
// Get updated InstanceID token.
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
Log.d(TAG, "Refreshed token: " + refreshedToken);
}
}
Section 230.5: Subscribe to a topic
Client apps can subscribe to any existing topic, or they can create a new topic. When a client app subscribes to a
new topic name, a new topic of that name is created in FCM and any client can subsequently subscribe to it.
To subscribe to a topic use the subscribeToTopic() method specifying the topic name:
FirebaseMessaging.getInstance().subscribeToTopic("myTopic");
GoalKicker.com – Android™ Notes for Professionals 1097
Chapter 231: Firebase Realtime DataBase
Section 231.1: Quick setup
1. Complete the Installation and setup part to connect your app to Firebase.
This will create the project in Firebase.
2. Add the dependency for Firebase Realtime Database to your module-level build.gradle file:
compile 'com.google.firebase:firebase-database:10.2.1'
3. Configure Firebase Database Rules
Now you are ready to work with the Realtime Database in Android.
For example you write a Hello World message to the database under the message key.
// Write a message to the database
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef = database.getReference("message");
myRef.setValue("Hello, World!");
Section 231.2: Firebase Realtime DataBase event handler
First Initialize FirebaseDatabase:
FirebaseDatabase database = FirebaseDatabase.getInstance();
Write to your database:
// Write a message to the database
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef = database.getReference("message");
myRef.setValue("Hello, World!");
Read from your database:
// Read from the database
myRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// This method is called once with the initial value and again
// whenever data at this location is updated.
String value = dataSnapshot.getValue(String.class);
Log.d(TAG, "Value is: " + value);
}
@Override
public void onCancelled(DatabaseError error) {
// Failed to read value
Log.w(TAG, "Failed to read value.", error.toException());
}
});
GoalKicker.com – Android™ Notes for Professionals 1098
Retrieve Data on Android events:
ChildEventListener childEventListener = new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
Log.d(TAG, "onChildAdded:" + dataSnapshot.getKey());
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
Log.d(TAG, "onChildChanged:" + dataSnapshot.getKey());
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
Log.d(TAG, "onChildRemoved:" + dataSnapshot.getKey());
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
Log.d(TAG, "onChildMoved:" + dataSnapshot.getKey());
}
@Override
public void onCancelled(DatabaseError databaseError) {
Log.w(TAG, "postComments:onCancelled", databaseError.toException());
Toast.makeText(mContext, "Failed to load comments.",
Toast.LENGTH_SHORT).show();
}
};
ref.addChildEventListener(childEventListener);
Section 231.3: Understanding firebase JSON database
Before we get our hands dirty with code, I feel it is necessary to understand how data is stored in firebase. Unlike
relational databases, firebase stores data in JSON format. Think of each row in a relational database as a JSON
object (which is basically unordered key-value pair). So the column name becomes key and the value stored in that
column for one particular row is the value. This way the entire row is represented as a JSON object and a list of
these represent an entire database table. The immediate benefit that I see for this is schema modification becomes
much more cheaper operation compared to old RDBMS. It is easier to add a couple of more attributes to a JSON
than altering a table structure.
here is a sample JSON to show how data is stored in firebase:
{
"user_base" : {
"342343" : {
"email" : "kaushal.xxxxx@gmail.com",
"authToken" : "some string",
"name" : "Kaushal",
"phone" : "+919916xxxxxx",
"serviceProviderId" : "firebase",
"signInServiceType" : "google",
},
"354895" : {
"email" : "xxxxx.devil@gmail.com",
"authToken" : "some string",
GoalKicker.com – Android™ Notes for Professionals 1099
"name" : "devil",
"phone" : "+919685xxxxxx",
"serviceProviderId" : "firebase",
"signInServiceType" : "github"
},
"371298" : {
"email" : "bruce.wayne@wayneinc.com",
"authToken" : "I am batman",
"name" : "Bruce Wayne",
"phone" : "+14085xxxxxx",
"serviceProviderId" : "firebase",
"signInServiceType" : "shield"
}
},
"user_prefs": {
"key1":{
"data": "for key one"
},
"key2":{
"data": "for key two"
},
"key3":{
"data": "for key three"
}
},
//other structures
}
This clearly shows how data that we used to store in relational databases can be stored in JSON format. Next let's
see how to read this data in android devices.
Section 231.4: Retrieving data from firebase
I am gonna assume you already know about adding gradle dependencies firebase in android studio. If you don't
just follow the guide from here. Add your app in firebase console, gradle sync android studio after adding
dependencies. All dependencies are not needed just firebase database and firebase auth.
Now that we know how data is stored and how to add gradle dependencies let's see how to use the imported
firebase android SDK to retrieve data.
create a firebase database reference
DatabaseReference userDBRef = FirebaseDatabase.getInstance().getReference();
// above statement point to base tree
userDBRef = DatabaseReference.getInstance().getReference().child("user_base")
// points to user_base table JSON (see previous section)
from here you can chain multiple child() method calls to point to the data you are interested in. For example if data
is stored as depicted in previous section and you want to point to Bruce Wayne user you can use:
DatabaseReference bruceWayneRef = userDBRef.child("371298");
// 371298 is key of bruce wayne user in JSON structure (previous section)
Or simply pass the whole reference to the JSON object:
DatabaseReference bruceWayneRef = DatabaseReference.getInstance().getReference()
.child("user_base/371298");
// deeply nested data can also be referenced this way, just put the fully
GoalKicker.com – Android™ Notes for Professionals 1100
// qualified path in pattern shown in above code "blah/blah1/blah1-2/blah1-2-3..."
Now that we have the reference of the data we want to fetch, we can use listeners to fetch data in android apps.
Unlike the traditional calls where you fire REST API calls using retrofit or volley, here a simple callback listener is
required to get the data. Firebase sdk calls the callback methods and you are done.
There are basically two types of listeners you can attach, one is ValueEventListener and the other one is
ChildEventListener (described in next section). For any change in data under the node we have references and
added listeners to, value event listeners return the entire JSON structure and child event listener returns specific
child where the change has happened. Both of these are useful in their own way. To fetch the data from firebase
we can add one or more listeners to a firebase database reference (list userDBRef we created earlier).
Here is some sample code (code explanation after code):
userDBRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
User bruceWayne = dataSnapshot.child("371298").getValue(User.class);
// Do something with the retrieved data or Bruce Wayne
}
@Override
public void onCancelled(DatabaseError databaseError) {
Log.e("UserListActivity", "Error occurred");
// Do something about the error
});
Did you notice the Class type passed. DataSnapshot can convert JSON data into our defined POJOs, simple pass the
right class type.
If your use case does not require the entire data (in our case user_base table) every time some little change occurs
or say you want to fetch the data only once, you can use addListenerForSingleValueEvent() method of Database
reference. This fires the callback only once.
userDBRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Do something
}
@Override
public void onCancelled(DatabaseError databaseError) {
// Do something about the error
});
Above samples will give you the value of the JSON node. To get the key simply call:
String myKey = dataSnapshot.getKey();
Section 231.5: Listening for child updates
Take a use case, like a chat app or a collaborative grocery list app (that basically requires a list of objects to be
synced across users). If you use firebase database and add a value event listener to the chat parent node or grocery
list parent node, you will end with entire chat structure from the beginning of time (i meant beginning of your chat)
every time a chat node is added (i.e. anyone says hi). That we don't want to do, what we are interested in is only the
new node or only the old node that got deleted or modified, the unchanged ones should not be returned.
GoalKicker.com – Android™ Notes for Professionals 1101
In this case we can use ChildEvenListener. Without any further adieu, here is code sample (see prev sections for
sample JSON data):
userDBRef.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
//If not dealing with ordered data forget about this
}
@Override
public void onCancelled(DatabaseError databaseError) {
});
Method names are self explanatory. As you can see whenever a new user is added or some property of existing
user is modified or user is deleted or removed appropriate callback method of child event listener is called with
relevant data. So if you are keeping UI refreshed for say chat app, get the JSON from onChildAdded() parse into
POJO and fit it in your UI. Just remember to remove your listener when user leaves the screen.
onChildChanged() gives the entire child value with changed properties (new ones).
onChiledRemoved() returns the removed child node.
Section 231.6: Retrieving data with pagination
When you have a huge JSON database, adding a value event listener doesn't make sense. It will return the huge
JSON and parsing it would be time consuming. In such cases we can use pagination and fetch part of data and
display or process it. Kind of like lazy loading or like fetching old chats when user clicks on show older chat. In this
case Query can used.
Let's take the our old example in previous sections. The user base contains 3 users, if it grows to say 3 hundred
thousand user and you want to fetch the user list in batches of 50:
// class level
final int limit = 50;
int start = 0;
// event level
Query userListQuery = userDBRef.orderByChild("email").limitToFirst(limit)
.startAt(start)
userListQuery.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Do something
start += (limit+1);
}
GoalKicker.com – Android™ Notes for Professionals 1102
@Override
public void onCancelled(DatabaseError databaseError) {
// Do something about the error
});
Here value or child events can be added and listened to. Call query again to fetch next 50. Make sure to add the
orderByChild() method, this will not work without that. Firebase needs to know the order by which you are
paginating.
Section 231.7: Denormalization: Flat Database Structure
Denormalization and a flat database structure is neccessary to efficiently download separate calls. With the
following structure, it is also possible to maintain two-way relationships. The disadvantage of this approach is, that
you always need to update the data in multiple places.
For an example, imagine an app which allows the user to store messages to himself (memos).
Desired flat database structure:
|--database
|-- memos
|-- memokey1
|-- title: "Title"
|-- content: "Message"
|-- memokey2
|-- title: "Important Title"
|-- content: "Important Message"
|-- users
|-- userKey1
|-- name: "John Doe"
|-- memos
|-- memokey1 : true //The values here don't matter, we only need the keys.
|-- memokey2 : true
|-- userKey2
|-- name: "Max Doe"
The used memo class
public class Memo {
private String title, content;
//getters and setters ...
//toMap() is necessary for the push process
private Map<String, Object> toMap() {
HashMap<String, Object> result = new HashMap<>();
result.put("title", title);
result.put("content", content);
return result;
}
}
Retrieving the memos of a user
//We need to store the keys and the memos separately
private ArrayList<String> mKeys = new ArrayList<>();
private ArrayList<Memo> mMemos = new ArrayList<>();
//The user needs to be logged in to retrieve the uid
GoalKicker.com – Android™ Notes for Professionals 1103
String currentUserId = FirebaseAuth.getInstance().getCurrentUser().getUid();
//This is the reference to the list of memos a user has
DatabaseReference currentUserMemoReference = FirebaseDatabase.getInstance().getReference()
.child("users").child(currentUserId).child("memos");
//This is a reference to the list of all memos
DatabaseReference memoReference = FirebaseDatabase.getInstance().getReference()
.child("memos");
//We start to listen to the users memos,
//this will also retrieve the memos initially
currentUserMemoReference.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
//Here we retrieve the key of the memo the user has.
String key = dataSnapshot.getKey(); //for example memokey1
//For later manipulations of the lists, we need to store the key in a list
mKeys.add(key);
//Now that we know which message belongs to the user,
//we request it from our memos:
memoReference.child(key).addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
//Here we retrieve our memo:
Memo memo = dataSnapshot.getValue(Memo.class);
mMemos.add(memo);
}
@Override
public void onCancelled(DatabaseError databaseError) { }
});
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) { }
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) { }
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) { }
@Override
public void onCancelled(DatabaseError databaseError) { }
}
Creating a memo
//The user needs to be logged in to retrieve the uid
String currentUserUid = FirebaseAuth.getInstance().getCurrentUser().getUid();
//This is the path to the list of memos a user has
String userMemoPath = "users/" + currentUserUid + "/memos/";
//This is the path to the list of all memos
String memoPath = "memos/";
//We need to retrieve an unused key from the memos reference
DatabaseReference memoReference = FirebaseDatabase.getInstance().getReference().child("memos");
String key = memoReference.push().getKey();
GoalKicker.com – Android™ Notes for Professionals 1104
Memo newMemo = new Memo("Important numbers", "1337, 42, 3.14159265359");
Map<String, Object> childUpdates = new HashMap<>();
//The second parameter **here** (the value) does not matter, it's just that the key exists
childUpdates.put(userMemoPath + key, true);
childUpdates.put(memoPath + key, newMemo.toMap());
FirebaseDatabase.getInstance().getReference().updateChildren(childUpdates);
After the push, or database looks like this:
|--database
|-- memos
|-- memokey1
|-- title: "Title"
|-- content: "Message"
|-- memokey2
|-- title: "Important Title"
|-- content: "Important Message"
|-- generatedMemokey3
|-- title: "Important numbers"
|-- content: "1337, 42, 3.14159265359"
|-- users
|-- userKey1
|-- name: "John Doe"
|-- memos
|-- memokey1 : true //The values here don't matter, we only need the keys.
|-- memokey2 : true
|-- generatedMemokey3 : true
|-- userKey2
|-- name: "Max Doe"
Section 231.8: Designing and understanding how to retrieve
realtime data from the Firebase Database
This example assumes that you have already set up a Firebase Realtime Database. If you are a starter, then please
inform yourself here on how to add Firebase to your Android project.
First, add the dependency of the Firebase Database to the app level build.gradle file:
compile 'com.google.firebase:firebase-database:9.4.0'
Now, let us create a chat app which stores data into the Firebase Database.
Step 1: Create a class named Chat
Just create a class with some basic variables required for the chat:
public class Chat{
public String name, message;
}
Step 2: Create some JSON data
For sending/retrieving data to/from the Firebase Database, you need to use JSON. Let us assume that some chats
are already stored at the root level in the database. The data of these chats could look like as follows:
[
GoalKicker.com – Android™ Notes for Professionals 1105
{
"name":"John Doe",
"message":"My first Message"
},
{
"name":"John Doe",
"message":"Second Message"
},
{
"name":"John Doe",
"message":"Third Message"
}
]
Step 3: Adding the listeners
There are three types of listeners. In the following example we are going to use the childEventListener:
DatabaseReference chatDb = FirebaseDatabase.getInstance().getReference() // Referencing the root of
the database.
.child("chats"); // Referencing the "chats" node under the root.
chatDb.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
// This function is called for every child id chat in this case, so using the above
// example, this function is going to be called 3 times.
// Retrieving the Chat object from this function is simple.
Chat chat; // Create a null chat object.
// Use the getValue function in the dataSnapshot and pass the object's class name to
// which you want to convert and get data. In this case it is Chat.class.
chat = dataSnapshot.getValue(Chat.class);
// Now you can use this chat object and add it into an ArrayList or something like
// that and show it in the recycler view.
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
// This function is called when any of the node value is changed, dataSnapshot will
// get the data with the key of the child, so you can swap the new value with the
// old one in the ArrayList or something like that.
// To get the key, use the .getKey() function.
// To get the value, use code similar to the above one.
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
// This function is called when any of the child node is removed. dataSnapshot will
// get the data with the key of the child.
// To get the key, use the s String parameter .
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
// This function is called when any of the child nodes is moved to a different position.
// To get the key, use the s String parameter.
GoalKicker.com – Android™ Notes for Professionals 1106
}
@Override
public void onCancelled(DatabaseError databaseError) {
// If anything goes wrong, this function is going to be called.
// You can get the exception by using databaseError.toException();
}
});
Step 4: Add data to the database
Just create a Chat class object and add the values as follows:
Chat chat=new Chat();
chat.name="John Doe";
chat.message="First message from android";
Now get a reference to the chats node as done in the retrieving session:
DatabaseReference chatDb = FirebaseDatabase.getInstance().getReference().child("chats");
Before you start adding data, keep in mind that you need one more deep reference since a chat node has several
more nodes and adding a new chat means adding a new node containing the chat details. We can generate a new
and unique name of the node using the push() function on the DatabaseReference object, which will return
another DatabaseReference, which in turn points to a newly formed node to insert the chat data.
Example
// The parameter is the chat object that was newly created a few lines above.
chatDb.push().setValue(chat);
The setValue() function will make sure that all of the application's onDataChanged functions are getting called
(including the same device), which happens to be the attached listener of the "chats" node.
GoalKicker.com – Android™ Notes for Professionals 1107
Chapter 232: Firebase App Indexing
Section 232.1: Supporting Http URLs
Step 1: Allow Google to Crawl to your content.Edit server’s robot.txt file.You can control google crawling for your
content by editing this file,you can refer to this link for more details.
Step 2: Associate your App with your website.Include assetlinks.json You upload it to your web server's .well-known
directory.Content of your assetlinks.json are as-
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target" :
{ "namespace": "android_app",
"package_name": "<your_package_name>",
"sha256_cert_fingerprints": ["<hash_of_app_certificate>"] }
}]
Step 3: Include App links in your manifest file to redirect Urls into your Application like below,
<activity
android:name=".activity.SampleActivity"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="example.live"
android:pathPrefix="/vod"
android:scheme="https"/>
<data
android:host="example.live"
android:pathPrefix="/vod"
android:scheme="http"/>
</intent-filter>
</activity>
Refer to this if you want learn about each and every tag here.
< action> Specify the ACTION_VIEW intent action so that the intent filter can be reached from Google Search.
< data> Add one or more tags, where each tag represents a URI format that resolves to the activity. At minimum,
the tag must include the android:scheme attribute. You can add additional attributes to further refine the type of
URI that the activity accepts. For example, you might have multiple activities that accept similar URIs, but which
differ simply based on the path name. In this case, use the android:path attribute or its variants (pathPattern or
pathPrefix) to differentiate which activity the system should open for different URI paths.
< category> Include the BROWSABLE category. The BROWSABLE category is required in order for the intent filter to
be accessible from a web browser. Without it, clicking a link in a browser cannot resolve to your app. The DEFAULT
category is optional, but recommended. Without this category, the activity can be started only with an explicit
intent, using your app component name.
Step 4: Handle incoming URLS
GoalKicker.com – Android™ Notes for Professionals 1108
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_schedule);
onNewIntent(getIntent());
}
protected void onNewIntent(Intent intent) {
String action = intent.getAction();
Uri data = intent.getData();
if (Intent.ACTION_VIEW.equals(action) && data != null) {
articleId = data.getLastPathSegment();
TextView linkText = (TextView)findViewById(R.id.link);
linkText.setText(data.toString());
}
}
Step 5: You can test this by using Android Debug Bridge command or studio configurations. Adb command: Launch
your application and then run this command:
adb shell am start -a android.intent.action.VIEW -d "{URL}" < package name >
Android Studio Configurations: Android studio > Build > Edit Configuration >Launch options>select URL>then
type in your Url here >Apply and test.Run your application if “Run” window shows error then you need to check
your URL format with your applinks mentioned in manifest otherwise it will successfully run,and redirect to page
mentioned your URL if specified.
Section 232.2: Add AppIndexing API
For Adding this to project you can find official doc easily but in this example I'm going to highlight some of the key
areas to be taken care of.
Step 1: Add google service
dependencies {
...
compile 'com.google.android.gms:play-services-appindexing:9.4.0'
...
}
Step 2: Import classes
import com.google.android.gms.appindexing.Action;
import com.google.android.gms.appindexing.AppIndex;
import com.google.android.gms.common.api.GoogleApiClient;
Step 3: Add App Indexing API calls
private GoogleApiClient mClient;
private Uri mUrl;
private String mTitle;
private String mDescription;
//If you know the values that to be indexed then you can initialize these variables in onCreate()
@Override
protected void onCreate(Bundle savedInstanceState) {
GoalKicker.com – Android™ Notes for Professionals 1109
mClient = new GoogleApiClient.Builder(this).addApi(AppIndex.API).build();
mUrl = "http://examplepetstore.com/dogs/standard-poodle";
mTitle = "Standard Poodle";
mDescription = "The Standard Poodle stands at least 18 inches at the withers";
}
//If your data is coming from a network request, then initialize these value in onResponse() and make
checks for NPE so that your code won’t fall apart.
//setting title and description for App Indexing
mUrl = Uri.parse(“android-app://com.famelive/https/m.fame.live/vod/” +model.getId());
mTitle = model.getTitle();
mDescription = model.getDescription();
mClient.connect();
AppIndex.AppIndexApi.start(mClient, getAction());
@Override
protected void onStop() {
if (mTitle != null && mDescription != null && mUrl != null) //if your response fails then check
whether these are initialized or not
if (getAction() != null) {
AppIndex.AppIndexApi.end(mClient, getAction());
mClient.disconnect();
}
super.onStop();
}
public Action getAction() {
Thing object = new Thing.Builder()
.setName(mTitle)
.setDescription(mDescription)
.setUrl(mUrl)
.build();
return new Action.Builder(Action.TYPE_WATCH)
.setObject(object)
.setActionStatus(Action.STATUS_TYPE_COMPLETED)
.build();
}
To test this just follow the step 4 in Remarks given below.
GoalKicker.com – Android™ Notes for Professionals 1110
Chapter 233: Firebase Crash Reporting
Section 233.1: How to report an error
Firebase Crash Reporting automatically generates reports for fatal errors (or uncaught exceptions).
You can create your custom report using:
FirebaseCrash.report(new Exception("My first Android non-fatal error"));
You can check in the log when FirebaseCrash initialized the module:
07–20 08:57:24.442 D/FirebaseCrashApiImpl: FirebaseCrash reporting API initialized 07–20
08:57:24.442 I/FirebaseCrash: FirebaseCrash reporting initialized
com.google.firebase.crash.internal.zzg@3333d325 07–20 08:57:24.442 D/FirebaseApp: Initialized class
com.google.firebase.crash.FirebaseCrash.
And then when it sent the exception:
07–20 08:57:47.052 D/FirebaseCrashApiImpl: throwable java.lang.Exception: My first Android nonfatal
error 07–20 08:58:18.822 D/FirebaseCrashSenderServiceImpl: Response code: 200 07–20
08:58:18.822 D/FirebaseCrashSenderServiceImpl: Report sent
You can add custom logs to your report with
FirebaseCrash.log("Activity created");
Section 233.2: How to add Firebase Crash Reporting to your
app
In order to add Firebase Crash Reporting to your app, perform the following steps:
Create an app on the Firebase Console here.
Copy the google-services.json file from your project into your in app/ directory.
Add the following rules to your root-level build.gradle file in order to include the google-services plugin:
buildscript {
// ...
dependencies {
// ...
classpath 'com.google.gms:google-services:3.0.0'
}
}
In your module Gradle file, add the apply plugin line at the bottom of the file to enable the Gradle plugin:
apply plugin: 'com.google.gms.google-services'
GoalKicker.com – Android™ Notes for Professionals 1111
Add the dependency for Crash Reporting to your app-level build.gradle file:
compile 'com.google.firebase:firebase-crash:10.2.1'
You can then fire a custom exception from your application by using the following line:
FirebaseCrash.report(new Exception("Non Fatal Error logging"));
All your fatal exceptions will be reported to your Firebase Console.
If you want to add custom logs to a console, you can use the following code:
FirebaseCrash.log("Level 2 completed.");
For more information, please visit:
Official documentation
Stack Overflow dedicated topic
GoalKicker.com – Android™ Notes for Professionals 1112
Chapter 234: Twitter APIs
Section 234.1: Creating login with twitter button and attach a
callback to it
1. Inside your layout, add a Login button with the following code:
<com.twitter.sdk.android.core.identity.TwitterLoginButton
android:id="@+id/twitter_login_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
2. In the Activity or Fragment that displays the button, you need to create and attach a Callback to the Login
Buttonas the following:
import com.twitter.sdk.android.core.Callback;
import com.twitter.sdk.android.core.Result;
import com.twitter.sdk.android.core.TwitterException;
import com.twitter.sdk.android.core.TwitterSession;
import com.twitter.sdk.android.core.identity.TwitterLoginButton;
...
loginButton = (TwitterLoginButton) findViewById(R.id.login_button);
loginButton.setCallback(new Callback<TwitterSession>() {
@Override
public void success(Result<TwitterSession> result) {
Log.d(TAG, "userName: " + session.getUserName());
Log.d(TAG, "userId: " + session.getUserId());
Log.d(TAG, "authToken: " + session.getAuthToken());
Log.d(TAG, "id: " + session.getId());
Log.d(TAG, "authToken: " + session.getAuthToken().token);
Log.d(TAG, "authSecret: " + session.getAuthToken().secret);
}
@Override
public void failure(TwitterException exception) {
// Do something on failure
}
});
3. Pass the result of the authentication Activity back to the button:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// Make sure that the loginButton hears the result from any
// Activity that it triggered.
loginButton.onActivityResult(requestCode, resultCode, data);
}
Note, If using the TwitterLoginButton in a Fragment, use the following steps instead:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
GoalKicker.com – Android™ Notes for Professionals 1113
super.onActivityResult(requestCode, resultCode, data);
// Pass the activity result to the fragment, which will then pass the result to the login
// button.
Fragment fragment = getFragmentManager().findFragmentById(R.id.your_fragment_id);
if (fragment != null) {
fragment.onActivityResult(requestCode, resultCode, data);
}
}
4. Add the following lines to your build.gradle dependencies:
apply plugin: 'io.fabric'
repositories {
maven { url 'https://maven.fabric.io/public' }
}
compile('com.twitter.sdk.android:twitter:1.14.1@aar') {
transitive = true;
}
GoalKicker.com – Android™ Notes for Professionals 1114
Chapter 235: Youtube-API
Section 235.1: Activity extending YouTubeBaseActivity
public class CustomYouTubeActivity extends YouTubeBaseActivity implements
YouTubePlayer.OnInitializedListener, YouTubePlayer.PlayerStateChangeListener {
private YouTubePlayerView mPlayerView;
private YouTubePlayer mYouTubePlayer;
private String mVideoId = "B08iLAtS3AQ";
private String mApiKey;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mApiKey = Config.YOUTUBE_API_KEY;
mPlayerView = new YouTubePlayerView(this);
mPlayerView.initialize(mApiKey, this); // setting up OnInitializedListener
addContentView(mPlayerView, new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT)); //show it in full screen
}
//Called when initialization of the player succeeds.
@Override
public void onInitializationSuccess(YouTubePlayer.Provider provider,
YouTubePlayer player,
boolean wasRestored) {
player.setPlayerStateChangeListener(this); // setting up the player state change listener
this.mYouTubePlayer = player;
if (!wasRestored)
player.cueVideo(mVideoId);
}
@Override
public void onInitializationFailure(YouTubePlayer.Provider provider,
YouTubeInitializationResult errorReason) {
Toast.makeText(this, "Error While initializing", Toast.LENGTH_LONG).show();
}
@Override
public void onAdStarted() {
}
@Override
public void onLoaded(String videoId) { //video has been loaded
if(!TextUtils.isEmpty(mVideoId) && !this.isFinishing() && mYouTubePlayer != null)
mYouTubePlayer.play(); // if we don't call play then video will not auto play, but user
still has the option to play via play button
}
@Override
public void onLoading() {
}
@Override
public void onVideoEnded() {
}
GoalKicker.com – Android™ Notes for Professionals 1115
@Override
public void onVideoStarted() {
}
@Override
public void onError(ErrorReason reason) {
Log.e("onError", "onError : " + reason.name());
}
}
Section 235.2: Consuming YouTube Data API on Android
This example will guide you how to get playlist data using the YouTube Data API on Android.
SHA-1 fingerprint
First you need to get an SHA-1 fingerprint for your machine. There are various methods for retrieving it. You can
choose any method provided in this Q&A.
Google API console and YouTube key for Android
Now that you have an SHA-1 fingerprint, open the Google API console and create a project. Go to this page and
create a project using that SHA-1 key and enable the YouTube Data API. Now you will get a key. This key will be used
to send requests from Android and fetch data.
Gradle part
You will have to add the following lines to your Gradle file for the YouTube Data API:
compile 'com.google.apis:google-api-services-youtube:v3-rev183-1.22.0'
In order to use YouTube's native client to send requests, we have to add the following lines in Gradle:
compile 'com.google.http-client:google-http-client-android:+'
compile 'com.google.api-client:google-api-client-android:+'
compile 'com.google.api-client:google-api-client-gson:+'
The following configuration also needs to be added in Gradle in order to avoid conflicts:
configurations.all {
resolutionStrategy.force 'com.google.code.findbugs:jsr305:3.0.2'
}
Below it is shown how the gradle.build would finally look like.
build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.aam.skillschool"
minSdkVersion 19
targetSdkVersion 25
GoalKicker.com – Android™ Notes for Professionals 1116
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
configurations.all {
resolutionStrategy.force 'com.google.code.findbugs:jsr305:3.0.2'
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.google.apis:google-api-services-youtube:v3-rev183-1.22.0'
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:support-v4:25.3.1'
compile 'com.google.http-client:google-http-client-android:+'
compile 'com.google.api-client:google-api-client-android:+'
compile 'com.google.api-client:google-api-client-gson:+'
}
Now comes the Java part. Since we will be using HttpTransport for networking and GsonFactory for converting
JSON into POJO, we don't need any other library to send any requests.
Now I want to show how to get playlists via the YouTube API by providing the playlist IDs. For this task I will use
AsyncTask. To understand how we request parameters and to understand the flow, please take a look at the
YouTube Data API.
public class GetPlaylistDataAsyncTask extends AsyncTask<String[], Void, PlaylistListResponse> {
private static final String YOUTUBE_PLAYLIST_PART = "snippet";
private static final String YOUTUBE_PLAYLIST_FIELDS = "items(id,snippet(title))";
private YouTube mYouTubeDataApi;
public GetPlaylistDataAsyncTask(YouTube api) {
mYouTubeDataApi = api;
}
@Override
protected PlaylistListResponse doInBackground(String[]... params) {
final String[] playlistIds = params[0];
PlaylistListResponse playlistListResponse;
try {
playlistListResponse = mYouTubeDataApi.playlists()
.list(YOUTUBE_PLAYLIST_PART)
.setId(TextUtils.join(",", playlistIds))
.setFields(YOUTUBE_PLAYLIST_FIELDS)
.setKey(AppConstants.YOUTUBE_KEY) //Here you will have to provide the keys
.execute();
} catch (IOException e) {
e.printStackTrace();
return null;
GoalKicker.com – Android™ Notes for Professionals 1117
}
return playlistListResponse;
}
}
The above asynchronous task will return an instance of PlaylistListResponse which is a build-in class of the
YouTube SDK. It has all the required fields, so we don't have to create POJOs ourself.
Finally, in our MainActivity we will have to do the following:
public class MainActivity extends AppCompatActivity {
private YouTube mYoutubeDataApi;
private final GsonFactory mJsonFactory = new GsonFactory();
private final HttpTransport mTransport = AndroidHttp.newCompatibleTransport();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_review);
mYoutubeDataApi = new YouTube.Builder(mTransport, mJsonFactory, null)
.setApplicationName(getResources().getString(R.string.app_name))
.build();
String[] ids = {"some playlists ids here separated by "," };
new GetPlaylistDataAsyncTask(mYoutubeDataApi) {
ProgressDialog progressDialog = new ProgressDialog(getActivity());
@Override
protected void onPreExecute() {
progressDialog.setTitle("Please wait.....");
progressDialog.show();
super.onPreExecute();
}
@Override
protected void onPostExecute(PlaylistListResponse playlistListResponse) {
super.onPostExecute(playlistListResponse);
//Here we get the playlist data
progressDialog.dismiss();
Log.d(TAG, playlistListResponse.toString());
}
}.execute(ids);
}
}
Section 235.3: Launching StandAlonePlayerActivity
1. Launch standalone player activity
Intent standAlonePlayerIntent = YouTubeStandalonePlayer.createVideoIntent((Activity)
context,
Config.YOUTUBE_API_KEY, // which you have created in step 3
videoId, // video which is to be played
100, //The time, in milliseconds, where playback should start in the video
true, //autoplay or not
false); //lightbox mode or not; false will show in fullscreen
context.startActivity(standAlonePlayerIntent);
GoalKicker.com – Android™ Notes for Professionals 1118
Section 235.4: YoutubePlayerFragment in portrait Activty
The following code implements a simple YoutubePlayerFragment. The activity's layout is locked in portrait mode
and when orientation changes or the user clicks full screen at the YoutubePlayer it turns to lansscape with the
YoutubePlayer filling the screen. The YoutubePlayerFragment does not need to extend an activity provided by the
Youtube library. It needs to implement YouTubePlayer.OnInitializedListener in order to get the YoutubePlayer
initialized. So our Activity's class is the following
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.Toast;
import com.google.android.youtube.player.YouTubeInitializationResult;
import com.google.android.youtube.player.YouTubePlayer;
import com.google.android.youtube.player.YouTubePlayerFragment;
public class MainActivity extends AppCompatActivity implements YouTubePlayer.OnInitializedListener
{
public static final String API_KEY ;
public static final String VIDEO_ID = "B08iLAtS3AQ";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
YouTubePlayerFragment youTubePlayerFragment = (YouTubePlayerFragment) getFragmentManager()
.findFragmentById(R.id.youtubeplayerfragment);
youTubePlayerFragment.initialize(API_KEY, this);
}
/**
*
* @param provider The provider which was used to initialize the YouTubePlayer
* @param youTubePlayer A YouTubePlayer which can be used to control video playback in the
provider.
* @param wasRestored Whether the player was restored from a previously saved state, as part of
the YouTubePlayerView
* or YouTubePlayerFragment restoring its state. true usually means playback
is resuming from where
* the user expects it would, and that a new video should not be loaded
*/
@Override
public void onInitializationSuccess(YouTubePlayer.Provider provider, YouTubePlayer
youTubePlayer, boolean wasRestored) {
youTubePlayer.setFullscreenControlFlags(YouTubePlayer.FULLSCREEN_FLAG_CONTROL_ORIENTATION |
YouTubePlayer.FULLSCREEN_FLAG_ALWAYS_FULLSCREEN_IN_LANDSCAPE);
if(!wasRestored) {
youTubePlayer.cueVideo(VIDEO_ID);
}
}
/**
GoalKicker.com – Android™ Notes for Professionals 1119
*
* @param provider The provider which failed to initialize a YouTubePlayer.
* @param error The reason for this failure, along with potential resolutions to this failure.
*/
@Override
public void onInitializationFailure(YouTubePlayer.Provider provider,
YouTubeInitializationResult error) {
final int REQUEST_CODE = 1;
if(error.isUserRecoverableError()) {
error.getErrorDialog(this,REQUEST_CODE).show();
} else {
String errorMessage = String.format("There was an error initializing the YoutubePlayer
(%1$s)", error.toString());
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
}
}
}
A YoutubePlayerFragment can be added to the activity's layout xaml as followed
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<fragment
android:id="@+id/youtubeplayerfragment"
android:name="com.google.android.youtube.player.YouTubePlayerFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:text="This is a YoutubePlayerFragment example"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
GoalKicker.com – Android™ Notes for Professionals 1120
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:text="This is a YoutubePlayerFragment example"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:text="This is a YoutubePlayerFragment example"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:text="This is a YoutubePlayerFragment example"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:text="This is a YoutubePlayerFragment example"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:text="This is a YoutubePlayerFragment example"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:text="This is a YoutubePlayerFragment example"
android:textStyle="bold"/>
</LinearLayout>
</ScrollView>
</LinearLayout>
Lastly you need to add the following attributes in your Manifest file inside the activity's tag
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="portrait"
Section 235.5: YouTube Player API
Obtaining the Android API Key :
First you'll need to get the SHA-1 fingerprint on your machine using java keytool. Execute the below command in
cmd/terminal to get the SHA-1 fingerprint.
GoalKicker.com – Android™ Notes for Professionals 1121
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -
keypass android
MainActivity.java
public class Activity extends YouTubeBaseActivity implements YouTubePlayer.OnInitializedListener {
private static final int RECOVERY_DIALOG_REQUEST = 1;
// YouTube player view
private YouTubePlayerView youTubeView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_main);
youTubeView = (YouTubePlayerView) findViewById(R.id.youtube_view);
// Initializing video player with developer key
youTubeView.initialize(Config.DEVELOPER_KEY, this);
}
@Override
public void onInitializationFailure(YouTubePlayer.Provider provider,
YouTubeInitializationResult errorReason) {
if (errorReason.isUserRecoverableError()) {
errorReason.getErrorDialog(this, RECOVERY_DIALOG_REQUEST).show();
} else {
String errorMessage = String.format(
getString(R.string.error_player), errorReason.toString());
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
}
}
@Override
public void onInitializationSuccess(YouTubePlayer.Provider provider,
YouTubePlayer player, boolean wasRestored) {
if (!wasRestored) {
// loadVideo() will auto play video
// Use cueVideo() method, if you don't want to play it automatically
player.loadVideo(Config.YOUTUBE_VIDEO_CODE);
// Hiding player controls
player.setPlayerStyle(YouTubePlayer.PlayerStyle.CHROMELESS);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RECOVERY_DIALOG_REQUEST) {
// Retry initialization if user performed a recovery action
getYouTubePlayerProvider().initialize(Config.DEVELOPER_KEY, this);
}
}
GoalKicker.com – Android™ Notes for Professionals 1122
private YouTubePlayer.Provider getYouTubePlayerProvider() {
return (YouTubePlayerView) findViewById(R.id.youtube_view);
}
}
Now create Config.java file. This file holds the Google Console API developer key and YouTube video id
Config.java
public class Config {
// Developer key
public static final String DEVELOPER_KEY = "AIzaSyDZtE10od_hXM5aXYEh6Zn7c6brV9ZjKuk";
// YouTube video id
public static final String YOUTUBE_VIDEO_CODE = "_oEA18Y8gM0";
}
xml file
<com.google.android.youtube.player.YouTubePlayerView
android:id="@+id/youtube_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp" />
GoalKicker.com – Android™ Notes for Professionals 1123
Chapter 236: Integrate Google Sign In
Parameter Detail
TAG A String used while logging
GoogleSignInHelper A static reference for helper
AppCompatActivity An Activity reference
GoogleApiClient A reference of GoogleAPIClient
RC_SIGN_IN An integer represents activity result constant
isLoggingOut A boolean to check if log-out task is running or not
Section 236.1: Google Sign In with Helper class
Add below to your build.gradle out of android tag:
// Apply plug-in to app.
apply plugin: 'com.google.gms.google-services'
Add below helper class to your util package:
/**
* Created by Andy
*/
public class GoogleSignInHelper implements GoogleApiClient.OnConnectionFailedListener,
GoogleApiClient.ConnectionCallbacks {
private static final String TAG = GoogleSignInHelper.class.getSimpleName();
private static GoogleSignInHelper googleSignInHelper;
private AppCompatActivity mActivity;
private GoogleApiClient mGoogleApiClient;
public static final int RC_SIGN_IN = 9001;
private boolean isLoggingOut = false;
public static GoogleSignInHelper newInstance(AppCompatActivity mActivity) {
if (googleSignInHelper == null) {
googleSignInHelper = new GoogleSignInHelper(mActivity, fireBaseAuthHelper);
}
return googleSignInHelper;
}
public GoogleSignInHelper(AppCompatActivity mActivity) {
this.mActivity = mActivity;
initGoogleSignIn();
}
private void initGoogleSignIn() {
// [START config_sign_in]
// Configure Google Sign In
GoogleSignInOptions gso = new
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(mActivity.getString(R.string.default_web_client_id))
.requestEmail()
.build();
// [END config_sign_in]
mGoogleApiClient = new GoogleApiClient.Builder(mActivity)
.enableAutoManage(mActivity /* FragmentActivity */, this /*
GoalKicker.com – Android™ Notes for Professionals 1124
OnConnectionFailedListener */)
.addApi(Auth.GOOGLE_SIGN_IN_API, gso)
.addConnectionCallbacks(this)
.build();
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
// An unresolvable error has occurred and Google APIs (including Sign-In) will not
// be available.
Log.d(TAG, "onConnectionFailed:" + connectionResult);
Toast.makeText(mActivity, "Google Play Services error.", Toast.LENGTH_SHORT).show();
}
public void getGoogleAccountDetails(GoogleSignInResult result) {
// Google Sign In was successful, authenticate with FireBase
GoogleSignInAccount account = result.getSignInAccount();
// You are now logged into Google
}
public void signOut() {
if (mGoogleApiClient.isConnected()) {
// Google sign out
Auth.GoogleSignInApi.signOut(mGoogleApiClient).setResultCallback(
new ResultCallback<Status>() {
@Override
public void onResult(@NonNull Status status) {
isLoggingOut = false;
}
});
} else {
isLoggingOut = true;
}
}
public GoogleApiClient getGoogleClient() {
return mGoogleApiClient;
}
@Override
public void onConnected(@Nullable Bundle bundle) {
Log.w(TAG, "onConnected");
if (isLoggingOut) {
signOut();
}
}
@Override
public void onConnectionSuspended(int i) {
Log.w(TAG, "onConnectionSuspended");
}
}
Add below code to your OnActivityResult in Activity file:
// [START onactivityresult]
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
GoalKicker.com – Android™ Notes for Professionals 1125
// Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
if (requestCode == GoogleSignInHelper.RC_SIGN_IN) {
GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
if (result.isSuccess()) {
googleSignInHelper.getGoogleAccountDetails(result);
} else {
// Google Sign In failed, update UI appropriately
// [START_EXCLUDE]
Log.d(TAG, "signInWith Google failed");
// [END_EXCLUDE]
}
}
}
// [END onactivityresult]
// [START signin]
public void signIn() {
Intent signInIntent =
Auth.GoogleSignInApi.getSignInIntent(googleSignInHelper.getGoogleClient());
startActivityForResult(signInIntent, GoogleSignInHelper.RC_SIGN_IN);
}
// [END signin]
GoalKicker.com – Android™ Notes for Professionals 1126
Chapter 237: Google signin integration on
android
This topic is based on How to integrate google sign-in, On android apps
Section 237.1: Integration of google Auth in your project. (Get
a configuration file)
First get the Configuration File for Sign-in from
Open link below
[https://developers.google.com/identity/sign-in/android/start-integrating][1]
click on get A configuration file
Enter App name And package name and click on choose and configure services
provide SHA1 Enable google SIGNIN and generate configuration files
Download the configuration file and place the file in app/ folder of your project
1. Add the dependency to your project-level build.gradle:
classpath 'com.google.gms:google-services:3.0.0'
2. Add the plugin to your app-level build.gradle:(bottom)
apply plugin: 'com.google.gms.google-services'
3. add this dependency to your app gradle file
dependencies { compile 'com.google.android.gms:play-services-auth:9.8.0' }
Section 237.2: Code Implementation Google SignIn
In your sign-in activity's onCreate method, configure Google Sign-In to request the user data required by your
app.
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.build();
create a GoogleApiClient object with access to the Google Sign-In API and the options you specified.
mGoogleApiClient = new GoogleApiClient.Builder(this)
.enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */)
.addApi(Auth.GOOGLE_SIGN_IN_API, gso)
.build();
Now When User click on Google signin button call this Function.
GoalKicker.com – Android™ Notes for Professionals 1127
private void signIn() {
Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
startActivityForResult(signInIntent, RC_SIGN_IN);
}
implement OnActivityResult to get the response.
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
if (requestCode == RC_SIGN_IN) {
GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
handleSignInResult(result);
}
}
Last step Handle The Result and get User Data
private void handleSignInResult(GoogleSignInResult result) {
Log.d(TAG, "handleSignInResult:" + result.isSuccess());
if (result.isSuccess()) {
// Signed in successfully, show authenticated UI.
GoogleSignInAccount acct = result.getSignInAccount();
mStatusTextView.setText(getString(R.string.signed_in_fmt, acct.getDisplayName()));
updateUI(true);
} else {
// Signed out, show unauthenticated UI.
updateUI(false);
}
}
GoalKicker.com – Android™ Notes for Professionals 1128
Chapter 238: Google Awareness APIs
Section 238.1: Get changes for location within a certain range
using Fence API
If you want to detect when your user enters a specific location, you can create a fence for the specific location with
a radius you want and be notified when your user enters or leaves the location.
// Your own action filter, like the ones used in the Manifest
private static final String FENCE_RECEIVER_ACTION = BuildConfig.APPLICATION_ID +
"FENCE_RECEIVER_ACTION";
private static final String FENCE_KEY = "locationFenceKey";
private FenceReceiver mFenceReceiver;
private PendingIntent mPendingIntent;
// Make sure to initialize your client as described in the Remarks section
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// etc
// The 0 is a standard Activity request code that can be changed for your needs
mPendingIntent = PendingIntent.getBroadcast(this, 0,
new Intent(FENCE_RECEIVER_ACTION), 0);
registerReceiver(mFenceReceiver, new IntentFilter(FENCE_RECEIVER_ACTION));
// Create the fence
AwarenessFence fence = LocationFence.entering(48.136334, 11.581660, 25);
// Register the fence to receive callbacks.
Awareness.FenceApi.updateFences(client, new FenceUpdateRequest.Builder()
.addFence(FENCE_KEY, fence, mPendingIntent)
.build())
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(@NonNull Status status) {
if (status.isSuccess()) {
Log.i(FENCE_KEY, "Successfully registered.");
} else {
Log.e(FENCE_KEY, "Could not be registered: " + status);
}
}
});
}
}
Now create a BroadcastReciver to recive updates in user state:
public class FenceReceiver extends BroadcastReceiver {
private static final String TAG = "FenceReceiver";
@Override
public void onReceive(Context context, Intent intent) {
// Get the fence state
FenceState fenceState = FenceState.extract(intent);
switch (fenceState.getCurrentState()) {
case FenceState.TRUE:
Log.i(TAG, "User is in location");
break;
GoalKicker.com – Android™ Notes for Professionals 1129
case FenceState.FALSE:
Log.i(TAG, "User is not in location");
break;
case FenceState.UNKNOWN:
Log.i(TAG, "User is doing something unknown");
break;
}
}
}
Section 238.2: Get current location using Snapshot API
// Remember to intialize your client as described in the Remarks section
Awareness.SnapshotApi.getLocation(client)
.setResultCallback(new ResultCallback<LocationResult>() {
@Override
public void onResult(@NonNull LocationResult locationResult) {
Location location = locationResult.getLocation();
Log.i(getClass().getSimpleName(), "Coordinates: "location.getLatitude() + "," +
location.getLongitude() + ", radius : " + location.getAccuracy());
}
});
Section 238.3: Get changes in user activity with Fence API
If you want to detect when your user starts or finishes an activity such as walking, running, or any other activity of
the DetectedActivityFence class, you can create a fence for the activity that you want to detect, and get notified
when your user starts/finishes this activity. By using a BroadcastReceiver, you will get an Intent with data that
contains the activity:
// Your own action filter, like the ones used in the Manifest.
private static final String FENCE_RECEIVER_ACTION = BuildConfig.APPLICATION_ID +
"FENCE_RECEIVER_ACTION";
private static final String FENCE_KEY = "walkingFenceKey";
private FenceReceiver mFenceReceiver;
private PendingIntent mPendingIntent;
// Make sure to initialize your client as described in the Remarks section.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// etc.
// The 0 is a standard Activity request code that can be changed to your needs.
mPendingIntent = PendingIntent.getBroadcast(this, 0,
new Intent(FENCE_RECEIVER_ACTION), 0);
registerReceiver(mFenceReceiver, new IntentFilter(FENCE_RECEIVER_ACTION));
// Create the fence.
AwarenessFence fence = DetectedActivityFence.during(DetectedActivityFence.WALKING);
// Register the fence to receive callbacks.
Awareness.FenceApi.updateFences(client, new FenceUpdateRequest.Builder()
.addFence(FENCE_KEY, fence, mPendingIntent)
.build())
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(@NonNull Status status) {
if (status.isSuccess()) {
Log.i(FENCE_KEY, "Successfully registered.");
} else {
GoalKicker.com – Android™ Notes for Professionals 1130
Log.e(FENCE_KEY, "Could not be registered: " + status);
}
}
});
}
}
Now you can receive the intent with a BroadcastReceiver to get callbacks when the user changes the activity:
public class FenceReceiver extends BroadcastReceiver {
private static final String TAG = "FenceReceiver";
@Override
public void onReceive(Context context, Intent intent) {
// Get the fence state
FenceState fenceState = FenceState.extract(intent);
switch (fenceState.getCurrentState()) {
case FenceState.TRUE:
Log.i(TAG, "User is walking");
break;
case FenceState.FALSE:
Log.i(TAG, "User is not walking");
break;
case FenceState.UNKNOWN:
Log.i(TAG, "User is doing something unknown");
break;
}
}
}
Section 238.4: Get current user activity using Snapshot API
For one-time, non-constant requests for a user's physical activity, use the Snapshot API:
// Remember to initialize your client as described in the Remarks section
Awareness.SnapshotApi.getDetectedActivity(client)
.setResultCallback(new ResultCallback<DetectedActivityResult>() {
@Override
public void onResult(@NonNull DetectedActivityResult detectedActivityResult) {
if (!detectedActivityResult.getStatus().isSuccess()) {
Log.e(getClass().getSimpleName(), "Could not get the current activity.");
return;
}
ActivityRecognitionResult result = detectedActivityResult
.getActivityRecognitionResult();
DetectedActivity probableActivity = result.getMostProbableActivity();
Log.i(getClass().getSimpleName(), "Activity received : " +
probableActivity.toString());
}
});
Section 238.5: Get headphone state with Snapshot API
// Remember to initialize your client as described in the Remarks section
Awareness.SnapshotApi.getHeadphoneState(client)
.setResultCallback(new ResultCallback<HeadphoneStateResult>() {
@Override
GoalKicker.com – Android™ Notes for Professionals 1131
public void onResult(@NonNull HeadphoneStateResult headphoneStateResult) {
Log.i(TAG, "Headphone state connection state: " +
headphoneStateResult.getHeadphoneState()
.getState() == HeadphoneState.PLUGGED_IN));
}
});
Section 238.6: Get nearby places using Snapshot API
// Remember to initialize your client as described in the Remarks section
Awareness.SnapshotApi.getPlaces(client)
.setResultCallback(new ResultCallback<PlacesResult>() {
@Override
public void onResult(@NonNull PlacesResult placesResult) {
List<PlaceLikelihood> likelihoodList = placesResult.getPlaceLikelihoods();
if (likelihoodList == null || likelihoodList.isEmpty()) {
Log.e(getClass().getSimpleName(), "No likely places");
}
}
});
As for getting the data in those places, here are some options:
Place place = placeLikelihood.getPlace();
String likelihood = placeLikelihood.getLikelihood();
Place place = likelihood.getPlace();
String placeName = place.getName();
String placeAddress = place.getAddress();
String placeCoords = place.getLatLng();
String locale = extractFromLocale(place.getLocale()));
Section 238.7: Get current weather using Snapshot API
// Remember to initialize your client as described in the Remarks section
Awareness.SnapshotApi.getWeather(client)
.setResultCallback(new ResultCallback<WeatherResult>() {
@Override
public void onResult(@NonNull WeatherResult weatherResult) {
Weather weather = weatherResult.getWeather();
if (weather == null) {
Log.e(getClass().getSimpleName(), "No weather received");
} else {
Log.i(getClass().getSimpleName(), "Temperature is " +
weather.getTemperature(Weather.CELSIUS) + ", feels like " +
weather.getFeelsLikeTemperature(Weather.CELSIUS) +
", humidity is " + weather.getHumidity());
}
}
});
GoalKicker.com – Android™ Notes for Professionals 1132
Chapter 239: Google Maps API v2 for
Android
Parameter Details
GoogleMap the GoogleMap is an object that is received on a onMapReady() event
MarkerOptions MarkerOptions is the builder class of a Marker, and is used to add one marker to a map.
Section 239.1: Custom Google Map Styles
Map Style
Google Maps come with a set of different styles to be applied, using this code :
// Sets the map type to be "hybrid"
map.setMapType(GoogleMap.MAP_TYPE_HYBRID);
The different map styles are :
Normal
map.setMapType(GoogleMap.MAP_TYPE_NORMAL);
Typical road map. Roads, some man-made features, and important natural features such as rivers are shown. Road
and feature labels are also visible.
GoalKicker.com – Android™ Notes for Professionals 1133
Hybrid
map.setMapType(GoogleMap.MAP_TYPE_HYBRID);
Satellite photograph data with road maps added. Road and feature labels are also visible.
Satellite
map.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
Satellite photograph data. Road and feature labels are not visible.
GoalKicker.com – Android™ Notes for Professionals 1134
Terrain
map.setMapType(GoogleMap.MAP_TYPE_TERRAIN);
Topographic data. The map includes colors, contour lines and labels, and perspective shading. Some roads and
labels are also visible.
GoalKicker.com – Android™ Notes for Professionals 1135
None
map.setMapType(GoogleMap.MAP_TYPE_NONE);
No tiles. The map will be rendered as an empty grid with no tiles loaded.
GoalKicker.com – Android™ Notes for Professionals 1136
OTHER STYLE OPTIONS
Indoor Maps
At high zoom levels, the map will show floor plans for indoor spaces. These are called indoor maps, and are
displayed only for the 'normal' and 'satellite' map types.
to enable or disable indoor maps, this is how it's done :
GoogleMap.setIndoorEnabled(true).
GoogleMap.setIndoorEnabled(false).
We can add custom styles to maps.
In onMapReady method add the following code snippet
mMap = googleMap;
try {
// Customise the styling of the base map using a JSON object defined
// in a raw resource file.
boolean success = mMap.setMapStyle(
MapStyleOptions.loadRawResourceStyle(
MapsActivity.this, R.raw.style_json));
if (!success) {
Log.e(TAG, "Style parsing failed.");
}
} catch (Resources.NotFoundException e) {
GoalKicker.com – Android™ Notes for Professionals 1137
Log.e(TAG, "Can't find style.", e);
}
under res folder create a folder name raw and add the styles json file. Sample style.json file
[
{
"featureType": "all",
"elementType": "geometry",
"stylers": [
{
"color": "#242f3e"
}
]
},
{
"featureType": "all",
"elementType": "labels.text.stroke",
"stylers": [
{
"lightness": -80
}
]
},
{
"featureType": "administrative",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#746855"
}
]
},
{
"featureType": "administrative.locality",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [
{
"color": "#263c3f"
}
]
},
{
GoalKicker.com – Android™ Notes for Professionals 1138
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#6b9a76"
}
]
},
{
"featureType": "road",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#2b3544"
}
]
},
{
"featureType": "road",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9ca5b3"
}
]
},
{
"featureType": "road.arterial",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#38414e"
}
]
},
{
"featureType": "road.arterial",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#212a37"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#746855"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#1f2835"
}
]
},
GoalKicker.com – Android™ Notes for Professionals 1139
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#f3d19c"
}
]
},
{
"featureType": "road.local",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#38414e"
}
]
},
{
"featureType": "road.local",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#212a37"
}
]
},
{
"featureType": "transit",
"elementType": "geometry",
"stylers": [
{
"color": "#2f3948"
}
]
},
{
"featureType": "transit.station",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [
{
"color": "#17263c"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#515c6d"
}
]
GoalKicker.com – Android™ Notes for Professionals 1140
},
{
"featureType": "water",
"elementType": "labels.text.stroke",
"stylers": [
{
"lightness": -20
}
]
}
]
To generate styles json file click this link
GoalKicker.com – Android™ Notes for Professionals 1141
GoalKicker.com – Android™ Notes for Professionals 1142
GoalKicker.com – Android™ Notes for Professionals 1143
Section 239.2: Default Google Map Activity
This Activity code will provide basic functionality for including a Google Map using a SupportMapFragment.
The Google Maps V2 API includes an all-new way to load maps.
Activities now have to implement the OnMapReadyCallBack interface, which comes with a onMapReady() method
override that is executed every time we run SupportMapFragment.getMapAsync(OnMapReadyCallback); and
the call is successfully completed.
Maps use Markers , Polygons and PolyLines to show interactive information to the user.
MapsActivity.java:
public class MapsActivity extends AppCompatActivity implements OnMapReadyCallback {
private GoogleMap mMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_maps);
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);
mapFragment.getMapAsync(this);
}
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
// Add a marker in Sydney, Australia, and move the camera.
LatLng sydney = new LatLng(-34, 151);
mMap.addMarker(new MarkerOptions().position(sydney).title("Marker in Sydney"));
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney));
}
}
Notice that the code above inflates a layout, which has a SupportMapFragment nested inside the container Layout,
defined with an ID of R.id.map. The layout file is shown below:
activity_maps.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:map="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/map"
tools:context="com.example.app.MapsActivity"
android:name="com.google.android.gms.maps.SupportMapFragment"/>
</LinearLayout>
GoalKicker.com – Android™ Notes for Professionals 1144
Section 239.3: Show Current Location in a Google Map
Here is a full Activity class that places a Marker at the current location, and also moves the camera to the current
position.
There are a few thing going on in sequence here:
Check Location permission
Once Location permission is granted, call setMyLocationEnabled(), build the GoogleApiClient, and connect it
Once the GoogleApiClient is connected, request location updates
public class MapLocationActivity extends AppCompatActivity
implements OnMapReadyCallback,
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
LocationListener {
GoogleMap mGoogleMap;
SupportMapFragment mapFrag;
LocationRequest mLocationRequest;
GoogleApiClient mGoogleApiClient;
Location mLastLocation;
Marker mCurrLocationMarker;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportActionBar().setTitle("Map Location Activity");
mapFrag = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map);
mapFrag.getMapAsync(this);
}
@Override
public void onPause() {
super.onPause();
//stop location updates when Activity is no longer active
if (mGoogleApiClient != null) {
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
}
}
@Override
public void onMapReady(GoogleMap googleMap)
{
mGoogleMap=googleMap;
mGoogleMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
//Initialize Google Play Services
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
//Location Permission already granted
buildGoogleApiClient();
mGoogleMap.setMyLocationEnabled(true);
} else {
GoalKicker.com – Android™ Notes for Professionals 1145
//Request Location Permission
checkLocationPermission();
}
}
else {
buildGoogleApiClient();
mGoogleMap.setMyLocationEnabled(true);
}
}
protected synchronized void buildGoogleApiClient() {
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();
mGoogleApiClient.connect();
}
@Override
public void onConnected(Bundle bundle) {
mLocationRequest = new LocationRequest();
mLocationRequest.setInterval(1000);
mLocationRequest.setFastestInterval(1000);
mLocationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient,
mLocationRequest, this);
}
}
@Override
public void onConnectionSuspended(int i) {}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {}
@Override
public void onLocationChanged(Location location)
{
mLastLocation = location;
if (mCurrLocationMarker != null) {
mCurrLocationMarker.remove();
}
//Place current location marker
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
MarkerOptions markerOptions = new MarkerOptions();
markerOptions.position(latLng);
markerOptions.title("Current Position");
markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_MAGENTA));
mCurrLocationMarker = mGoogleMap.addMarker(markerOptions);
//move map camera
mGoogleMap.moveCamera(CameraUpdateFactory.newLatLng(latLng));
mGoogleMap.animateCamera(CameraUpdateFactory.zoomTo(11));
//stop location updates
if (mGoogleApiClient != null) {
GoalKicker.com – Android™ Notes for Professionals 1146
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
}
}
public static final int MY_PERMISSIONS_REQUEST_LOCATION = 99;
private void checkLocationPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.ACCESS_FINE_LOCATION)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(this)
.setTitle("Location Permission Needed")
.setMessage("This app needs the Location permission, please accept to use
location functionality")
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//Prompt the user once explanation has been shown
ActivityCompat.requestPermissions(MapLocationActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
MY_PERMISSIONS_REQUEST_LOCATION );
}
})
.create()
.show();
} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
MY_PERMISSIONS_REQUEST_LOCATION );
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_LOCATION: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// location-related task you need to do.
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
if (mGoogleApiClient == null) {
buildGoogleApiClient();
}
mGoogleMap.setMyLocationEnabled(true);
}
GoalKicker.com – Android™ Notes for Professionals 1147
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
Toast.makeText(this, "permission denied", Toast.LENGTH_LONG).show();
}
return;
}
// other 'case' lines to check for other
// permissions this app might request
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:map="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/map"
tools:context="com.example.app.MapLocationActivity"
android:name="com.google.android.gms.maps.SupportMapFragment"/>
</LinearLayout>
Result:
Show explanation if needed on Marshmallow and Nougat using an AlertDialog (this case happens when the user
had previously denied a permission request, or had granted the permission and then later revoked it in the
settings):
GoalKicker.com – Android™ Notes for Professionals 1148
Prompt the user for Location permission on Marshmallow and Nougat by calling
ActivityCompat.requestPermissions():
GoalKicker.com – Android™ Notes for Professionals 1149
Move camera to current location and place Marker when the Location permission is granted:
GoalKicker.com – Android™ Notes for Professionals 1150
Section 239.4: Change Oset
By changing mappoint x and y values as you need you can change offset possition of google map,by default it will
be in the center of the map view. Call below method where you want to change it! Better to use it inside your
onLocationChanged like changeOffsetCenter(location.getLatitude(),location.getLongitude());
public void changeOffsetCenter(double latitude,double longitude) {
Point mappoint = mGoogleMap.getProjection().toScreenLocation(new LatLng(latitude,
longitude));
mappoint.set(mappoint.x, mappoint.y-100); // change these values as you need , just hard
coded a value if you want you can give it based on a ratio like using DisplayMetrics as well
mGoogleMap.animateCamera(CameraUpdateFactory.newLatLng(mGoogleMap.getProjection().fromScreenLocatio
n(mappoint)));
}
Section 239.5: MapView: embedding a GoogleMap in an
existing layout
It is possible to treat a GoogleMap as an Android view if we make use of the provided MapView class. Its usage is
very similar to MapFragment.
In your layout use MapView as follows:
<com.google.android.gms.maps.MapView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:map="http://schemas.android.com/apk/res-auto"
GoalKicker.com – Android™ Notes for Professionals 1151
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
<!--
map:mapType="0" Specifies a change to the initial map type
map:zOrderOnTop="true" Control whether the map view's surface is placed on top of its window
map:useVieLifecycle="true" When using a MapFragment, this flag specifies whether the lifecycle
of the map should be tied to the fragment's view or the fragment itself
map:uiCompass="true" Enables or disables the compass
map:uiRotateGestures="true" Sets the preference for whether rotate gestures should be enabled or
disabled
map:uiScrollGestures="true" Sets the preference for whether scroll gestures should be enabled or
disabled
map:uiTiltGestures="true" Sets the preference for whether tilt gestures should be enabled or
disabled
map:uiZoomGestures="true" Sets the preference for whether zoom gestures should be enabled or
disabled
map:uiZoomControls="true" Enables or disables the zoom controls
map:liteMode="true" Specifies whether the map should be created in lite mode
map:uiMapToolbar="true" Specifies whether the mapToolbar should be enabled
map:ambientEnabled="true" Specifies whether ambient-mode styling should be enabled
map:cameraMinZoomPreference="0.0" Specifies a preferred lower bound for camera zoom
map:cameraMaxZoomPreference="1.0" Specifies a preferred upper bound for camera zoom -->
/>
Your activity needs to implement the OnMapReadyCallback interface in order to work:
/**
* This shows how to create a simple activity with a raw MapView and add a marker to it. This
* requires forwarding all the important lifecycle methods onto MapView.
*/
public class RawMapViewDemoActivity extends AppCompatActivity implements OnMapReadyCallback {
private MapView mMapView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.raw_mapview_demo);
mMapView = (MapView) findViewById(R.id.map);
mMapView.onCreate(savedInstanceState);
mMapView.getMapAsync(this);
}
@Override
protected void onResume() {
super.onResume();
mMapView.onResume();
}
@Override
public void onMapReady(GoogleMap map) {
map.addMarker(new MarkerOptions().position(new LatLng(0, 0)).title("Marker"));
}
@Override
protected void onPause() {
mMapView.onPause();
super.onPause();
GoalKicker.com – Android™ Notes for Professionals 1152
}
@Override
protected void onDestroy() {
mMapView.onDestroy();
super.onDestroy();
}
@Override
public void onLowMemory() {
super.onLowMemory();
mMapView.onLowMemory();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mMapView.onSaveInstanceState(outState);
}
}
Section 239.6: Get debug SHA1 fingerprint
1. Open Android Studio
2. Open Your Project
3. Click on Gradle (From Right Side Panel, you will see Gradle Bar)
4. Click on Refresh (Click on Refresh from Gradle Bar, you will see List Gradle scripts of your Project)
5. Click on Your Project (Your Project Name form List (root))
6. Click on Tasks
7. Click on android
8. Double Click on signingReport (You will get SHA1 and MD5 in Run Bar)
GoalKicker.com – Android™ Notes for Professionals 1153
Section 239.7: Adding markers to a map
To add markers to a Google Map, for example from an ArrayList of MyLocation Objects, we can do it this way.
The MyLocation holder class:
public class MyLocation {
LatLng latLng;
String title;
String snippet;
}
Here is a method that would take a list of MyLocation Objects and place a Marker for each one:
private void LocationsLoaded(List<MyLocation> locations){
for (MyLocation myLoc : locations){
mMap.addMarker(new MarkerOptions()
.position(myLoc.latLng)
.title(myLoc.title)
.snippet(myLoc.snippet)
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_MAGENTA));
}
}
Note: For the purpose of this example, mMap is a class member variable of the Activity, where we've assigned it to
the map reference received in the onMapReady() override.
Section 239.8: UISettings
Using UISettings, the appearance of the Google Map can be modified.
Here is an example of some common settings:
mGoogleMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
mGoogleMap.getUiSettings().setMapToolbarEnabled(true);
mGoogleMap.getUiSettings().setZoomControlsEnabled(true);
mGoogleMap.getUiSettings().setCompassEnabled(true);
Result:
GoalKicker.com – Android™ Notes for Professionals 1154
Section 239.9: InfoWindow Click Listener
Here is an example of how to define a different action for each Marker's InfoWindow click event.
Use a HashMap in which the marker ID is the key, and the value is the corresponding action it should take when the
InfoWindow is clicked.
Then, use a OnInfoWindowClickListener to handle the event of a user clicking the InfoWindow, and use the
HashMap to determine which action to take.
In this simple example we will open up a different Activity based on which Marker's InfoWindow was clicked.
Declare the HashMap as an instance variable of the Activity or Fragment:
//Declare HashMap to store mapping of marker to Activity
HashMap<String, String> markerMap = new HashMap<String, String>();
Then, each time you add a Marker, make an entry in the HashMap with the Marker ID and the action it should take
when it's InfoWindow is clicked.
For example, adding two Markers and defining an action to take for each:
Marker markerOne = googleMap.addMarker(new MarkerOptions().position(latLng1)
.title("Marker One")
.snippet("This is Marker One");
String idOne = markerOne.getId();
markerMap.put(idOne, "action_one");
GoalKicker.com – Android™ Notes for Professionals 1155
Marker markerTwo = googleMap.addMarker(new MarkerOptions().position(latLng2)
.title("Marker Two")
.snippet("This is Marker Two");
String idTwo = markerTwo.getId();
markerMap.put(idTwo, "action_two");
In the InfoWindow click listener, get the action from the HashMap, and open up the corresponding Activity based
on the action of the Marker:
mGoogleMap.setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() {
@Override
public void onInfoWindowClick(Marker marker) {
String actionId = markerMap.get(marker.getId());
if (actionId.equals("action_one")) {
Intent i = new Intent(MainActivity.this, ActivityOne.class);
startActivity(i);
} else if (actionId.equals("action_two")) {
Intent i = new Intent(MainActivity.this, ActivityTwo.class);
startActivity(i);
}
}
});
Note If the code is in a Fragment, replace MainActivity.this with getActivity().
Section 239.10: Obtaining the SH1-Fingerprint of your
certificate keystore file
In order to obtain a Google Maps API key for your certificate, you must provide the API console with the SH1-
fingerprint of your debug/release keystore.
You can obtain the keystore by using the JDK's keytool program as described here in the docs.
Another approach is to obtain the fingerprint programmatically by running this snippet with your app signed with
the debug/release certificate and printing the hash to the log.
PackageInfo info;
try {
info = getPackageManager().getPackageInfo("com.package.name", PackageManager.GET_SIGNATURES);
for (Signature signature : info.signatures) {
MessageDigest md;
md = MessageDigest.getInstance("SHA");
md.update(signature.toByteArray());
String hash= new String(Base64.encode(md.digest(), 0));
Log.e("hash", hash);
}
} catch (NameNotFoundException e1) {
Log.e("name not found", e1.toString());
} catch (NoSuchAlgorithmException e) {
Log.e("no such an algorithm", e.toString());
} catch (Exception e) {
Log.e("exception", e.toString());
}
GoalKicker.com – Android™ Notes for Professionals 1156
Section 239.11: Do not launch Google Maps when the map is
clicked (lite mode)
When a Google Map is displayed in lite mode clicking on a map will open the Google Maps application. To disable
this functionality you must call setClickable(false) on the MapView, e.g.:
final MapView mapView = (MapView)view.findViewById(R.id.map);
mapView.setClickable(false);
GoalKicker.com – Android™ Notes for Professionals 1157
Chapter 240: Google Drive API
Google Drive is a file hosting service created by Google. It provides file storage service and allows the user to
upload files in the cloud and also share with other people. Using Google Drive API, we can synchronize files
between computer or mobile device and Google Drive Cloud.
Section 240.1: Integrate Google Drive in Android
Create a New Project on Google Developer Console
To integrate Android application with Google Drive, create the credentials of project in the Google Developers
Console. So, we need to create a project on Google Developer console.
To create a project on Google Developer Console, follow these steps:
Go to Google Developer Console for Android. Fill your project name in the input field and click on the create
button to create a new project on Google Developer console.
We need to create credentials to access API. So, click on the Create credentials button.
GoalKicker.com – Android™ Notes for Professionals 1158
Now, a pop window will open. Click on API Key option in the list to create API key.
We need an API key to call Google APIs for Android. So, click on the Android Key to identify your Android
Project.
GoalKicker.com – Android™ Notes for Professionals 1159
Next, we need to add Package Name of the Android Project and SHA-1 fingerprint in the input fields to
create API key.
We need to generate SHA-1 fingerprint. So, open your terminal and run Keytool utility to get the SHA1
fingerprint. While running Keytool utility, you need to provide keystore password. Default development
keytool password is “android”. keytool -exportcert -alias androiddebugkey -keystore
~/.android/debug.keystore -list -v
GoalKicker.com – Android™ Notes for Professionals 1160
Now, add Package name and SHA-1 fingerprint in input fields on credentials page. Finally, click on create
button to create API key.
This will create API key for Android. We will use the this API key to integrate Android application with Google
Drive.
GoalKicker.com – Android™ Notes for Professionals 1161
Enable Google Drive API
We need to enable Google Drive Api to access files stored on Google Drive from Android application. To enable
Google Drive API, follow below steps:
Go to your Google Developer console Dashboard and click on Enable APIs get credentials like keys then
you will see popular Google APIs.
GoalKicker.com – Android™ Notes for Professionals 1162
Click on Drive API link to open overview page of Google Drive API.
Click on the Enable button to enable Google drive API. It allows client access to Google Drive.
GoalKicker.com – Android™ Notes for Professionals 1163
Add Internet Permission
App needs Internet access Google Drive files. Use the following code to set up Internet permissions in
AndroidManifest.xml file :
<uses-permission android:name="android.permission.INTERNET" />
Add Google Play Services
We will use Google play services API which includes the Google Drive Android API. So, we need to setup Google
play services SDK in Android Application. Open your build.gradle(app module) file and add Google play services
SDK as a dependencies.
dependencies {
....
compile 'com.google.android.gms:play-services:<latest_version>'
....
}
Add API key in Manifest file
To use Google API in Android application, we need to add API key and version of the Google Play Service in the
AndroidManifest.xml file. Add the correct metadata tags inside the tag of the AndroidManifest.xml file.
Connect and Authorize the Google Drive Android API
We need to authenticate and connect Google Drive Android API with Android application. Authorization of Google
Drive Android API is handled by the GoogleApiClient. We will use GoogleApiClient within onResume() method.
/**
* Called when the activity will start interacting with the user.
* At this point your activity is at the top of the activity stack,
GoalKicker.com – Android™ Notes for Professionals 1164
* with user input going to it.
*/
@Override
protected void onResume() {
super.onResume();
if (mGoogleApiClient == null) {
/**
* Create the API client and bind it to an instance variable.
* We use this instance as the callback for connection and connection failures.
* Since no account name is passed, the user is prompted to choose.
*/
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Drive.API)
.addScope(Drive.SCOPE_FILE)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
mGoogleApiClient.connect();
}
Disconnect Google Deive Android API
When activity stops, we will disconnected Google Drive Android API connection with Android application by calling
disconnect() method inside activity’s onStop() method.
@Override
protected void onStop() {
super.onStop();
if (mGoogleApiClient != null) {
// disconnect Google Android Drive API connection.
mGoogleApiClient.disconnect();
}
super.onPause();
}
Implement Connection Callbacks and Connection Failed Listener
We will implement Connection Callbacks and Connection Failed Listener of Google API client in MainActivity.java file
to know status about connection of Google API client. These listeners provide onConnected(),
onConnectionFailed(), onConnectionSuspended() method to handle the connection issues between app and
Drive.
If user has authorized the application, the onConnected() method is invoked. If user has not authorized
application, onConnectionFailed() method is invoked and a dialog is displayed to user that your app is not
authorized to access Google Drive. In case connection is suspended, onConnectionSuspended() method is called.
You need to implement ConnectionCallbacks and OnConnectionFailedListener in your activity. Use the following
code in your Java file.
@Override
public void onConnectionFailed(ConnectionResult result) {
// Called whenever the API client fails to connect.
Log.i(TAG, "GoogleApiClient connection failed:" + result.toString());
GoalKicker.com – Android™ Notes for Professionals 1165
if (!result.hasResolution()) {
// show the localized error dialog.
GoogleApiAvailability.getInstance().getErrorDialog(this, result.getErrorCode(),
0).show();
return;
}
/**
* The failure has a resolution. Resolve it.
* Called typically when the app is not yet authorized, and an authorization
* dialog is displayed to the user.
*/
try {
result.startResolutionForResult(this, REQUEST_CODE_RESOLUTION);
} catch (SendIntentException e) {
Log.e(TAG, "Exception while starting resolution activity", e);
}
}
/**
* It invoked when Google API client connected
* @param connectionHint
*/
@Override
public void onConnected(Bundle connectionHint) {
Toast.makeText(getApplicationContext(), "Connected", Toast.LENGTH_LONG).show();
}
/**
* It invoked when connection suspended
* @param cause
*/
@Override
public void onConnectionSuspended(int cause) {
Log.i(TAG, "GoogleApiClient connection suspended");
}
Section 240.2: Create a File on Google Drive
We will add a file on Google Drive. We will use the createFile() method of a Drive object to create file
programmatically on Google Drive. In this example we are adding a new text file in the user’s root folder. When a
file is added, we need to specify the initial set of metadata, file contents, and the parent folder.
We need to create a CreateMyFile() callback method and within this method, use the Drive object to retrieve a
DriveContents resource. Then we pass the API client to the Drive object and call the driveContentsCallback
callback method to handle result of DriveContents.
A DriveContents resource contains a temporary copy of the file's binary stream which is only available to the
application.
public void CreateMyFile(){
fileOperation = true;
GoalKicker.com – Android™ Notes for Professionals 1166
// Create new contents resource.
Drive.DriveApi.newDriveContents(mGoogleApiClient)
.setResultCallback(driveContentsCallback);
}
Result Handler of DriveContents
Handling the response requires to check if the call was successful or not. If the call was successful, we can retrieve
the DriveContents resource.
We will create a result handler of DriveContents. Within this method, we call the CreateFileOnGoogleDrive()
method and pass the result of DriveContentsResult:
/**
* This is the Result result handler of Drive contents.
* This callback method calls the CreateFileOnGoogleDrive() method.
*/
final ResultCallback<DriveContentsResult> driveContentsCallback =
new ResultCallback<DriveContentsResult>() {
@Override
public void onResult(DriveContentsResult result) {
if (result.getStatus().isSuccess()) {
if (fileOperation == true){
CreateFileOnGoogleDrive(result);
}
}
}
};
Create File Programmatically
To create files, we need to use a MetadataChangeSet object. By using this object, we set the title (file name) and file
type. Also, we must use the createFile() method of the DriveFolder class and pass the Google client API, the
MetaDataChangeSet object, and the driveContents to create a file. We call the result handler callback to handle the
result of the created file.
We use the following code to create a new text file in the user's root folder:
/**
* Create a file in the root folder using a MetadataChangeSet object.
* @param result
*/
public void CreateFileOnGoogleDrive(DriveContentsResult result){
final DriveContents driveContents = result.getDriveContents();
// Perform I/O off the UI thread.
new Thread() {
@Override
public void run() {
// Write content to DriveContents.
OutputStream outputStream = driveContents.getOutputStream();
Writer writer = new OutputStreamWriter(outputStream);
try {
writer.write("Hello Christlin!");
writer.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
MetadataChangeSet changeSet = new MetadataChangeSet.Builder()
GoalKicker.com – Android™ Notes for Professionals 1167
.setTitle("My First Drive File")
.setMimeType("text/plain")
.setStarred(true).build();
// Create a file in the root folder.
Drive.DriveApi.getRootFolder(mGoogleApiClient)
.createFile(mGoogleApiClient, changeSet, driveContents)
setResultCallback(fileCallback);
}
}.start();
}
Handle result of Created File
The following code will create a callback method to handle the result of the created file:
/**
* Handle result of Created file
*/
final private ResultCallback<DriveFolder.DriveFileResult> fileCallback = new
ResultCallback<DriveFolder.DriveFileResult>() {
@Override
public void onResult(DriveFolder.DriveFileResult result) {
if (result.getStatus().isSuccess()) {
Toast.makeText(getApplicationContext(), "file created: "+
result.getDriveFile().getDriveId(), Toast.LENGTH_LONG).show();
}
return;
}
};
GoalKicker.com – Android™ Notes for Professionals 1168
Chapter 241: Displaying Google Ads
Section 241.1: Adding Interstitial Ad
Interstitial ads are full-screen ads that cover the interface of their host app. They're typically displayed at natural
transition points in the flow of an app, such as between activities or during the pause between levels in a game.
Make sure you have necessary permissions in your Manifest file:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
1. Go to your AdMob account.
2. Click on Monetize tab.
3. Select or Create the app and choose the platform.
4. Select Interstitial and give an ad unit name.
5. Once the ad unit is created, you can notice the Ad unit ID on the dashboard. For example: ca-apppub-
00000000000/000000000
6. Add dependencies
compile 'com.google.firebase:firebase-ads:10.2.1'
This one should be on the bottom.
apply plugin: 'com.google.gms.google-services'
Add your Ad unit ID to your strings.xml file
<string name="interstitial_full_screen">ca-app-pub-00000000/00000000</string>
Add ConfigChanges and meta-data to your manifest:
<activity
android:name="com.google.android.gms.ads.AdActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestS
creenSize"
android:theme="@android:style/Theme.Translucent" />
and
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
Activity:
public class AdActivity extends AppCompatActivity {
private String TAG = AdActivity.class.getSimpleName();
InterstitialAd mInterstitialAd;
GoalKicker.com – Android™ Notes for Professionals 1169
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
mInterstitialAd = new InterstitialAd(this);
// set the ad unit ID
mInterstitialAd.setAdUnitId(getString(R.string.interstitial_full_screen));
AdRequest adRequest = new AdRequest.Builder()
.build();
// Load ads into Interstitial Ads
mInterstitialAd.loadAd(adRequest);
mInterstitialAd.setAdListener(new AdListener() {
public void onAdLoaded() {
showInterstitial();
}
});
}
private void showInterstitial() {
if (mInterstitialAd.isLoaded()) {
mInterstitialAd.show();
}
}
}
This AdActivity will show a full screen ad now.
Section 241.2: Basic Ad Setup
You'll need to add the following to your dependencies:
compile 'com.google.firebase:firebase-ads:10.2.1'
and then put this in the same file.
apply plugin: 'com.google.gms.google-services'
Next you'll need to add relevant information into your strings.xml.
<string name="banner_ad_unit_id">ca-app-pub-####/####</string>
Next place an adview wherever you want it and style it just like any other view.
<com.google.android.gms.ads.AdView
android:id="@+id/adView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
ads:adSize="BANNER"
ads:adUnitId="@string/banner_ad_unit_id">
</com.google.android.gms.ads.AdView>
GoalKicker.com – Android™ Notes for Professionals 1170
And last but not least, throw this in your onCreate.
MobileAds.initialize(getApplicationContext(), "ca-app-pub-YOUR_ID");
AdView mAdView = (AdView) findViewById(R.id.adView);
AdRequest adRequest = new AdRequest.Builder().build();
mAdView.loadAd(adRequest);
If you copy-pasted exactly you should now have a small banner ad. Simply place more AdViews wherever you need
them for more.
GoalKicker.com – Android™ Notes for Professionals 1171
Chapter 242: AdMob
Param Details
ads:adUnitId="@string/main_screen_ad"
The ID of your ad. Get your ID from the admob site. "While it's not a
requirement, storing your ad unit ID values in a resource file is a good
practice. As your app grows and your ad publishing needs mature, it may be
necessary to change the ID values. If you keep them in a resource file, you
never have to search through your code looking for them.".[1]
Section 242.1: Implementing
Note: This example requires a valid Admob account and valid Admob ad code.
Build.gradle on app level
Change to the latest version if existing:
compile 'com.google.firebase:firebase-ads:10.2.1'
Manifest
Internet permission is required to access the ad data. Note that this permission does not have to be requested
(using API 23+) as it is a normal permission and not dangerous:
<uses-permission android:name="android.permission.INTERNET" />
XML
The following XML example shows a banner ad:
<com.google.android.gms.ads.AdView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/adView"
ads:adSize="BANNER"
ads:adUnitId="@string/main_screen_ad" />
For the code of other types, refer to the Google AdMob Help.
Java
The following code is for the integration of banner ads. Note that other ad types may require different integration:
// Alternative for faster initialization.
// MobileAds.initialize(getApplicationContext(), "AD_UNIT_ID");
AdView mAdView = (AdView) findViewById(R.id.adView);
// Add your device test ID if you are doing testing before releasing.
// The device test ID can be found in the admob stacktrace.
AdRequest adRequest = new AdRequest.Builder().build();
mAdView.loadAd(adRequest);
Add the AdView life cycle methods in the onResume(), onPause(), and onDestroy() methods of your activity:
@Override
public void onPause() {
if (mAdView != null) {
mAdView.pause();
GoalKicker.com – Android™ Notes for Professionals 1172
}
super.onPause();
}
@Override
public void onResume() {
super.onResume();
if (mAdView != null) {
mAdView.resume();
}
}
@Override
public void onDestroy() {
if (mAdView != null) {
mAdView.destroy();
}
super.onDestroy();
}
GoalKicker.com – Android™ Notes for Professionals 1173
Chapter 243: Google Play Store
Section 243.1: Open Google Play Store Listing for your app
The following code snippet shows how to open the Google Play Store Listing of your app in a safe way. Usually you
want to use it when asking the user to leave a review for your app.
private void openPlayStore() {
String packageName = getPackageName();
Intent playStoreIntent = new Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=" + packageName));
setFlags(playStoreIntent);
try {
startActivity(playStoreIntent);
} catch (Exception e) {
Intent webIntent = new Intent(Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=" + packageName));
setFlags(webIntent);
startActivity(webIntent);
}
}
@SuppressWarnings("deprecation")
private void setFlags(Intent intent) {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
else
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
}
Note: The code opens the Google Play Store if the app is installed. Otherwise it will just open the web browser.
Section 243.2: Open Google Play Store with the list of all
applications from your publisher account
You can add a "Browse Our Other Apps" button in your app, listing all your(publisher) applications in the Google
Play Store app.
String urlApp = "market://search?q=pub:Google+Inc.";
String urlWeb = "http://play.google.com/store/search?q=pub:Google+Inc.";
try {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(urlApp));
setFlags(i);
startActivity(i);
} catch (android.content.ActivityNotFoundException anfe) {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(urlWeb)));
setFlags(i);
startActivity(i);
}
@SuppressWarnings("deprecation")
public void setFlags(Intent i) {
i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
i.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
}
GoalKicker.com – Android™ Notes for Professionals 1174
else {
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
}
}
GoalKicker.com – Android™ Notes for Professionals 1175
Chapter 244: Sign your Android App for
Release
Android requires that all APKs be signed for release.
Section 244.1: Sign your App
1. In the menu bar, click Build > Generate Signed APK.
2. Select the module you would like to release from the drop down and click Next.
3. To Create a new keystore, click Create new. Now fill the required information and press ok in New Key Store.
GoalKicker.com – Android™ Notes for Professionals 1176
4. On the Generate Signed APK Wizard fields are already populated for you if you just created new key store
otherwise fill it and click next.
5. On the next window, select a destination for the signed APK, select the build type and click finish.
Section 244.2: Configure the build.gradle with signing
configuration
You can define the signing configuration to sign the apk in the build.gradle file.
You can define:
storeFile : the keystore file
storePassword: the keystore password
keyAlias: a key alias name
keyPassword: A key alias password
You have to define the signingConfigs block to create a signing configuration:
android {
signingConfigs {
myConfig {
storeFile file("myFile.keystore")
storePassword "xxxx"
keyAlias "xxxx"
keyPassword "xxxx"
}
}
//....
}
Then you can assign it to one or more build types.
android {
buildTypes {
GoalKicker.com – Android™ Notes for Professionals 1177
release {
signingConfig signingConfigs.myConfig
}
}
}
GoalKicker.com – Android™ Notes for Professionals 1178
Chapter 245: TensorFlow
TensorFlow was designed with mobile and embedded platforms in mind. We have sample code and build support
you can try now for these platforms:
Android iOS Raspberry Pi
Section 245.1: How to use
Install Bazel from here. Bazel is the primary build system for TensorFlow. Now, edit the WORKSPACE, we can find
the WORKSPACE file in the root directory of the TensorFlow that we have cloned earlier.
# Uncomment and update the paths in these entries to build the Android demo.
#android_sdk_repository(
# name = "androidsdk",
# api_level = 23,
# build_tools_version = "25.0.1",
# # Replace with path to Android SDK on your system
# path = "<PATH_TO_SDK>",
#)
#
#android_ndk_repository(
# name="androidndk",
# path="<PATH_TO_NDK>",
# api_level=14)
Like below with our sdk and ndk path:
android_sdk_repository(
name = "androidsdk",
api_level = 23,
build_tools_version = "25.0.1",
# Replace with path to Android SDK on your system
path = "/Users/amitshekhar/Library/Android/sdk/",
)
android_ndk_repository(
name="androidndk",
path="/Users/amitshekhar/Downloads/android-ndk-r13/",
api_level=14)
GoalKicker.com – Android™ Notes for Professionals 1179
Chapter 246: Android Vk Sdk
Section 246.1: Initialization and login
1. Create a new application here: create application
2. Choose standalone applicaton and confirm app creation via SMS.
3. Fill Package namefor Android as your current package name. You can get your package name inside android
manifest file, at the very begginning.
4. Get your Certificate fingerprint by executing this command in your shell/cmd:
keytool -exportcert -alias androiddebugkey -keystore path-to-debug-or-production-keystore -list -v
You can also get this fingerprint by SDK itself:
String[] fingerprints = VKUtil.getCertificateFingerprint(this, this.getPackageName());
Log.d("MainActivity", fingerprints[0]);
5. Add recieved fingerprint into your Signing certificate fingerprint for Android: field in Vk app settings
(where you entered your package name)
6. Then add this to your gradle file:
compile 'com.vk:androidsdk:1.6.5'
8. Initialize the SDK on startup using the following method. The best way is to call it in the Applications onCreate
method.
private static final int VK_ID = your_vk_id;
public static final String VK_API_VERSION = "5.52"; //current version
@Override
public void onCreate() {
super.onCreate();
VKSdk.customInitialize(this, VK_ID, VK_API_VERSION);
}
This is the best way to initizlize VKSdk. Don't use the methid where VK_ID should be placed inside strings.xml
because api will not work correctly after it.
9. Final step is to login using vksdk.
public static final String[] VK_SCOPES = new String[]{
VKScope.FRIENDS,
VKScope.MESSAGES,
VKScope.NOTIFICATIONS,
VKScope.OFFLINE,
VKScope.STATUS,
VKScope.STATS,
VKScope.PHOTOS
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
someButtonForLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GoalKicker.com – Android™ Notes for Professionals 1180
VKSdk.login(this, VK_SCOPES);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
VKSdk.onActivityResult(requestCode, resultCode, data, new VKCallback<VKAccessToken>() {
@Override
public void onResult(VKAccessToken res) {
res.accessToken; //getting our token here.
}
@Override
public void onError(VKError error) {
Toast.makeText(SocialNetworkChooseActivity.this,
"User didn't pass Authorization", Toast.LENGTH_SHORT).show();
}
});
}
GoalKicker.com – Android™ Notes for Professionals 1181
Chapter 247: Project SDK versions
Parameter Details
SDK Version The SDK version for each field is the Android release's SDK API level integer. For example, Froyo
(Android 2.2) corresponds to API level 8. These integers are also defined in Build.VERSION_CODES.
An Android application needs to run on all kinds of devices. Each device may have a different version on Android
running on it.
Now, each Android version might not support all the features that your app requires, and so while building an app,
you need to keep the minimum and maximum Android version in mind.
Section 247.1: Defining project SDK versions
In your build.gradle file of main module(app), define your minimum and target version number.
android {
//the version of sdk source used to compile your project
compileSdkVersion 23
defaultConfig {
//the minimum sdk version required by device to run your app
minSdkVersion 19
//you normally don't need to set max sdk limit so that your app can support future versions
of android without updating app
//maxSdkVersion 23
//
//the latest sdk version of android on which you are targeting(building and testing) your
app, it should be same as compileSdkVersion
targetSdkVersion 23
}
}
GoalKicker.com – Android™ Notes for Professionals 1182
Chapter 248: Facebook SDK for Android
Parameter Details
TAG A String used while logging
FacebookSignInHelper A static reference to facebook helper
CallbackManager A callback for facebook operations
Activity A context
PERMISSION_LOGIN An array that contains all permission required from facebook to login.
loginCallback A callback for facebook login
Section 248.1: How to add Facebook Login in Android
Add below dependencies to your build.gradle
// Facebook login
compile 'com.facebook.android:facebook-android-sdk:4.21.1'
Add below helper class to your utility package:
/**
* Created by Andy
* An utility for Facebook
*/
public class FacebookSignInHelper {
private static final String TAG = FacebookSignInHelper.class.getSimpleName();
private static FacebookSignInHelper facebookSignInHelper = null;
private CallbackManager callbackManager;
private Activity mActivity;
private static final Collection<String> PERMISSION_LOGIN = (Collection<String>)
Arrays.asList("public_profile", "user_friends","email");
private FacebookCallback<LoginResult> loginCallback;
public static FacebookSignInHelper newInstance(Activity context) {
if (facebookSignInHelper == null)
facebookSignInHelper = new FacebookSignInHelper(context);
return facebookSignInHelper;
}
public FacebookSignInHelper(Activity mActivity) {
try {
this.mActivity = mActivity;
// Initialize the SDK before executing any other operations,
// especially, if you're using Facebook UI elements.
FacebookSdk.sdkInitialize(this.mActivity);
callbackManager = CallbackManager.Factory.create();
loginCallback = new FacebookCallback<LoginResult>() {
@Override
public void onSuccess(LoginResult loginResult) {
// You are logged into Facebook
}
@Override
public void onCancel() {
Log.d(TAG, "Facebook: Cancelled by user");
GoalKicker.com – Android™ Notes for Professionals 1183
}
@Override
public void onError(FacebookException error) {
Log.d(TAG, "FacebookException: " + error.getMessage());
}
};
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* To login user on facebook without default Facebook button
*/
public void loginUser() {
try {
LoginManager.getInstance().registerCallback(callbackManager, loginCallback);
LoginManager.getInstance().logInWithReadPermissions(this.mActivity, PERMISSION_LOGIN);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* To log out user from facebook
*/
public void signOut() {
// Facebook sign out
LoginManager.getInstance().logOut();
}
public CallbackManager getCallbackManager() {
return callbackManager;
}
public FacebookCallback<LoginResult> getLoginCallback() {
return loginCallback;
}
/**
* Attempts to log debug key hash for facebook
*
* @param context : A reference to context
* @return : A facebook debug key hash
*/
public static String getKeyHash(Context context) {
String keyHash = null;
try {
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(),
PackageManager.GET_SIGNATURES);
for (Signature signature : info.signatures) {
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(signature.toByteArray());
keyHash = Base64.encodeToString(md.digest(), Base64.DEFAULT);
Log.d(TAG, "KeyHash:" + keyHash);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
GoalKicker.com – Android™ Notes for Professionals 1184
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return keyHash;
}
}
Add below code in Your Activity:
FacebookSignInHelper facebookSignInHelper = FacebookSignInHelper.newInstance(LoginActivity.this,
fireBaseAuthHelper);
facebookSignInHelper.loginUser();
Add below code to your OnActivityResult:
facebookSignInHelper.getCallbackManager().onActivityResult(requestCode, resultCode, data);
Section 248.2: Create your own custom button for Facebook
login
Once you first add the Facebook login/signup, the button looks something like:
Most of the times, it doesn't match with the design-specs of your app. And here's how you can customize it:
<FrameLayout
android:layout_below="@+id/no_network_bar"
android:id="@+id/FrameLayout1"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.facebook.login.widget.LoginButton
android:id="@+id/login_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone" />
<Button
android:background="#3B5998"
android:layout_width="match_parent"
android:layout_height="60dp"
android:id="@+id/fb"
android:onClick="onClickFacebookButton"
android:textAllCaps="false"
android:text="Sign up with Facebook"
android:textSize="22sp"
android:textColor="#ffffff" />
</FrameLayout>
Just wrap the original com.facebook.login.widget.LoginButton into a FrameLayout and make its visibility gone.
Next, add your custom button in the same FrameLayout. I've added some sample specs. You can always make your
own drawable background for the facebook button and set it as the background of the button.
GoalKicker.com – Android™ Notes for Professionals 1185
The final thing we do is simply convert the click on my custom button to a click on the facecbook button:
//The original Facebook button
LoginButton loginButton = (LoginButton)findViewById(R.id.login_button);
//Our custom Facebook button
fb = (Button) findViewById(R.id.fb);
public void onClickFacebookButton(View view) {
if (view == fb) {
loginButton.performClick();
}
}
Great! Now the button looks something like this:
Section 248.3: A minimalistic guide to Facebook login/signup
implementation
1. You have to setup the prerequisites.
2. Add the Facebook activity to the AndroidManifest.xml file:
<activity
android:name="com.facebook.FacebookActivity"
android:configChanges= "keyboard|keyboardHidden|screenLayout|screenSize|orientation"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:label="@string/app_name" />
3. Add the login button to your layout XML file:
<com.facebook.login.widget.LoginButton
android:id="@+id/login_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
4. Now you have the Facebook button. If the user clicks on it, the Facebook login dialog will come up on top of
the app's screen. Here the user can fill in their credentials and press the Log In button. If the credentials are
correct, the dialog grants the corresponding permissions and a callback is sent to your original activity
containing the button. The following code shows how you can receive that callback:
loginButton.registerCallback(callbackManager, new FacebookCallback<LoginResult>() {
@Override
public void onSuccess(LoginResult loginResult) {
// Completed without error. You might want to use the retrieved data here.
}
@Override
public void onCancel() {
// The user either cancelled the Facebook login process or didn't authorize the app.
}
@Override
GoalKicker.com – Android™ Notes for Professionals 1186
public void onError(FacebookException exception) {
// The dialog was closed with an error. The exception will help you recognize what
exactly went wrong.
}
});
Section 248.4: Setting permissions to access data from the
Facebook profile
If you want to retrieve the details of a user's Facebook profile, you need to set permissions for the same:
loginButton = (LoginButton)findViewById(R.id.login_button);
loginButton.setReadPermissions(Arrays.asList("email", "user_about_me"));
You can keep adding more permissions like friends-list, posts, photos etc. Just pick the right permission and add it
the above list.
Note: You don't need to set any explicit permissions for accessing the public profile (first name, last name, id,
gender etc).
Section 248.5: Logging out of Facebook
Facebook SDK 4.0 onwards, this is how we logout:
com.facebook.login.LoginManager.getInstance().logOut();
For versions before 4.0, the logging out is gone by explicitly clearing the access token:
Session session = Session.getActiveSession();
session.closeAndClearTokenInformation();
GoalKicker.com – Android™ Notes for Professionals 1187
Chapter 249: Thread
Section 249.1: Thread Example with its description
While launching an application firstly main thread is executed. This Main thread handles all the UI concept of
application. If we want to run long the task in which we don't need the UI then we use thread for running that task
in background.
Here is the example of Thread which describes blow:
new Thread(new Runnable() {
public void run() {
for(int i = 1; i < 5;i++) {
System.out.println(i);
}
}
}).start();
We can create thread by creating the object of Thread which have Thread.run() method for running the
thread.Here, run() method is called by the start() method.
We can also run the the multiple threads independently, which is known as MultiThreading. This thread also have
the functionality of sleep by which the currently executing thread to sleep (temporarily cease execution) for the
specified number of time. But sleep throws the InterruptedException So, we have to handle it by using try/catch like
this.
try{Thread.sleep(500);}catch(InterruptedException e){System.out.println(e);}
Section 249.2: Updating the UI from a Background Thread
It is common to use a background Thread for doing network operations or long running tasks, and then update the
UI with the results when needed.
This poses a problem, as only the main thread can update the UI.
The solution is to use the runOnUiThread() method, as it allows you to initiate code execution on the UI thread
from a background Thread.
In this simple example, a Thread is started when the Activity is created, runs until the magic number of 42 is
randomly generated, and then uses the runOnUiThread() method to update the UI once this condition is met.
public class MainActivity extends AppCompatActivity {
TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.my_text_view);
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
GoalKicker.com – Android™ Notes for Professionals 1188
//do stuff....
Random r = new Random();
if (r.nextInt(100) == 42) {
break;
}
}
runOnUiThread(new Runnable() {
@Override
public void run() {
mTextView.setText("Ready Player One");
}
});
}
}).start();
}
}
GoalKicker.com – Android™ Notes for Professionals 1189
Chapter 250: AsyncTask
Parameter Details
Params the type of the parameters sent to the task upon execution.
Progress the type of the progress units published during the background computation
Result the type of the result of the background computation.
Section 250.1: Basic Usage
In Android Activities and Services, most callbacks are run on the main thread. This makes it simple to update the UI,
but running processor- or I/O-heavy tasks on the main thread can cause your UI to pause and become
unresponsive (official documentation on what then happens).
You can remedy this by putting these heavier tasks on a background thread.
One way to do this is using an AsyncTask, which provides a framework to facilitate easy usage of a background
Thread, and also perform UI Thread tasks before, during, and after the background Thread has completed its work.
Methods that can be overridden when extending AsyncTask:
onPreExecute() : invoked on the UI thread before the task is executed
doInBackground(): invoked on the background thread immediately after onPreExecute() finishes
executing.
onProgressUpdate(): invoked on the UI thread after a call to publishProgress(Progress...).
onPostExecute(): invoked on the UI thread after the background computation finishes
Example
public class MyCustomAsyncTask extends AsyncTask<File, Void, String> {
@Override
protected void onPreExecute(){
// This runs on the UI thread before the background thread executes.
super.onPreExecute();
// Do pre-thread tasks such as initializing variables.
Log.v("myBackgroundTask", "Starting Background Task");
}
@Override
protected String doInBackground(File... params) {
// Disk-intensive work. This runs on a background thread.
// Search through a file for the first line that contains "Hello", and return
// that line.
try (Scanner scanner = new Scanner(params[0])) {
while (scanner.hasNextLine()) {
final String line = scanner.nextLine();
publishProgress(); // tell the UI thread we made progress
if (line.contains("Hello")) {
return line;
}
}
return null;
}
}
GoalKicker.com – Android™ Notes for Professionals 1190
@Override
protected void onProgressUpdate(Void...p) {
// Runs on the UI thread after publishProgress is invoked
Log.v("Read another line!")
}
@Override
protected void onPostExecute(String s) {
// This runs on the UI thread after complete execution of the doInBackground() method
// This function receives result(String s) returned from the doInBackground() method.
// Update UI with the found string.
TextView view = (TextView) findViewById(R.id.found_string);
if (s != null) {
view.setText(s);
} else {
view.setText("Match not found.");
}
}
}
Usage:
MyCustomAsyncTask asyncTask = new MyCustomAsyncTask<File, Void, String>();
// Run the task with a user supplied filename.
asyncTask.execute(userSuppliedFilename);
or simply:
new MyCustomAsyncTask().execute(userSuppliedFilename);
Note
When defining an AsyncTask we can pass three types between < > brackets.
Defined as <Params, Progress, Result> (see Parameters section)
In the previous example we've used types <File, Void, String>:
AsyncTask<File, Void, String>
// Params has type File
// Progress has unused type
// Result has type String
Void is used when you want to mark a type as unused.
Note that you can't pass primitive types (i.e. int, float and 6 others) as parameters. In such cases, you should pass
their wrapper classes, e.g. Integer instead of int, or Float instead of float.
The AsyncTask and Activity life cycle
AsyncTasks don't follow Activity instances' life cycle. If you start an AsyncTask inside an Activity and you rotate the
device, the Activity will be destroyed and a new instance will be created. But the AsyncTask will not die. It will go on
living until it completes.
Solution: AsyncTaskLoader
One subclass of Loaders is the AsyncTaskLoader. This class performs the same function as the AsyncTask, but much
better. It can handle Activity configuration changes more easily, and it behaves within the life cycles of Fragments
and Activities. The nice thing is that the AsyncTaskLoader can be used in any situation that the AsyncTask is being
used. Anytime data needs to be loaded into memory for the Activity/Fragment to handle, The AsyncTaskLoader can
GoalKicker.com – Android™ Notes for Professionals 1191
do the job better.
Section 250.2: Pass Activity as WeakReference to avoid
memory leaks
It is common for an AsyncTask to require a reference to the Activity that called it.
If the AsyncTask is an inner class of the Activity, then you can reference it and any member variables/methods
directly.
If, however, the AsyncTask is not an inner class of the Activity, you will need to pass an Activity reference to the
AsyncTask. When you do this, one potential problem that may occur is that the AsyncTask will keep the reference of
the Activity until the AsyncTask has completed its work in its background thread. If the Activity is finished or killed
before the AsyncTask's background thread work is done, the AsyncTask will still have its reference to the Activity,
and therefore it cannot be garbage collected.
As a result, this will cause a memory leak.
In order to prevent this from happening, make use of a WeakReference in the AsyncTask instead of having a direct
reference to the Activity.
Here is an example AsyncTask that utilizes a WeakReference:
private class MyAsyncTask extends AsyncTask<String, Void, Void> {
private WeakReference<Activity> mActivity;
public MyAsyncTask(Activity activity) {
mActivity = new WeakReference<Activity>(activity);
}
@Override
protected void onPreExecute() {
final Activity activity = mActivity.get();
if (activity != null) {
....
}
}
@Override
protected Void doInBackground(String... params) {
//Do something
String param1 = params[0];
String param2 = params[1];
return null;
}
@Override
protected void onPostExecute(Void result) {
final Activity activity = mActivity.get();
if (activity != null) {
activity.updateUI();
}
}
}
Calling the AsyncTask from an Activity:
GoalKicker.com – Android™ Notes for Professionals 1192
new MyAsyncTask(this).execute("param1", "param2");
Calling the AsyncTask from a Fragment:
new MyAsyncTask(getActivity()).execute("param1", "param2");
Section 250.3: Download Image using AsyncTask in Android
This tutorial explains how to download Image using AsyncTask in Android. The example below download image
while showing progress bar while during download. Understanding Android AsyncTask
Async task enables you to implement MultiThreading without get Hands dirty into threads. AsyncTask enables
proper and easy use of the UI thread. It allows performing background operations and passing the results on the UI
thread. If you are doing something isolated related to UI, for example downloading data to present in a list, go
ahead and use AsyncTask.
AsyncTasks should ideally be used for short operations (a few seconds at the most.)
An asynchronous task is defined by 3 generic types, called Params, Progress and Result, and 4 steps,
called onPreExecute(), doInBackground(), onProgressUpdate() and onPostExecute().
In onPreExecute() you can define code, which need to be executed before background processing starts.
doInBackground have code which needs to be executed in background, here in doInBackground() we can
send results to multiple times to event thread by publishProgress() method, to notify background processing
has been completed we can return results simply.
onProgressUpdate() method receives progress updates from doInBackground() method, which is published
via publishProgress() method, and this method can use this progress update to update event thread
onPostExecute() method handles results returned by doInBackground() method.
The generic types used are
Params, the type of the parameters sent to the task upon execution
Progress, the type of the progress units published during the background computation.
Result, the type of the result of the background computation.
If an async task not using any types, then it can be marked as Void type.
An running async task can be cancelled by calling cancel(boolean) method.
Downloading image using Android AsyncTask
your .xml layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<Button
android:id="@+id/downloadButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Click Here to Download" />
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="Your image will appear here" />
GoalKicker.com – Android™ Notes for Professionals 1193
</LinearLayout>
.java class
package com.javatechig.droid;
import java.io.InputStream;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import android.app.Activity;
import android.app.ProgressDialog;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
public class ImageDownladerActivity extends Activity {
private ImageView downloadedImg;
private ProgressDialog simpleWaitDialog;
private String downloadUrl = "http://www.9ori.com/store/media/images/8ab579a656.jpg";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.asynch);
Button imageDownloaderBtn = (Button) findViewById(R.id.downloadButton);
downloadedImg = (ImageView) findViewById(R.id.imageView);
imageDownloaderBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
new ImageDownloader().execute(downloadUrl);
}
});
}
private class ImageDownloader extends AsyncTask {
@Override
protected Bitmap doInBackground(String... param) {
// TODO Auto-generated method stub
return downloadBitmap(param[0]);
}
@Override
protected void onPreExecute() {
Log.i("Async-Example", "onPreExecute Called");
simpleWaitDialog = ProgressDialog.show(ImageDownladerActivity.this,
GoalKicker.com – Android™ Notes for Professionals 1194
"Wait", "Downloading Image");
}
@Override
protected void onPostExecute(Bitmap result) {
Log.i("Async-Example", "onPostExecute Called");
downloadedImg.setImageBitmap(result);
simpleWaitDialog.dismiss();
}
private Bitmap downloadBitmap(String url) {
// initilize the default HTTP client object
final DefaultHttpClient client = new DefaultHttpClient();
//forming a HttpGet request
final HttpGet getRequest = new HttpGet(url);
try {
HttpResponse response = client.execute(getRequest);
//check 200 OK for success
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
Log.w("ImageDownloader", "Error " + statusCode +
" while retrieving bitmap from " + url);
return null;
}
final HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream inputStream = null;
try {
// getting contents from the stream
inputStream = entity.getContent();
// decoding stream data back into image Bitmap that android understands
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
} finally {
if (inputStream != null) {
inputStream.close();
}
entity.consumeContent();
}
}
} catch (Exception e) {
// You Could provide a more explicit error message for IOException
getRequest.abort();
Log.e("ImageDownloader", "Something went wrong while" +
" retrieving bitmap from " + url + e.toString());
}
return null;
}
}
}
GoalKicker.com – Android™ Notes for Professionals 1195
Since there is currently no comment field for examples (or I haven't found it or I haven't permission for it) here is
some comment about this:
This is a good example what can be done with AsyncTask.
However the example currently has problems with
possible memory leaks
app crash if there was a screen rotation shortly before the async task finished.
For details see:
Pass Activity as WeakReference to avoid memory leaks
http://stackoverflow.com/documentation/android/117/asynctask/5377/possible-problems-with-inner-async-t
asks
Avoid leaking Activities with AsyncTask
Section 250.4: Canceling AsyncTask
YourAsyncTask task = new YourAsyncTask();
task.execute();
task.cancel();
This doesn't stop your task if it was in progress, it just sets the cancelled flag which can be checked by checking the
return value of isCancelled() (assuming your code is currently running) by doing this:
class YourAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
while(!isCancelled()) {
... doing long task stuff
//Do something, you need, upload part of file, for example
if (isCancelled()) {
return null; // Task was detected as canceled
}
if (yourTaskCompleted) {
return null;
}
}
}
}
Note
If an AsyncTask is canceled while doInBackground(Params... params) is still executing then the method
onPostExecute(Result result) will NOT be called after doInBackground(Params... params) returns. The
AsyncTask will instead call the onCancelled(Result result) to indicate that the task was cancelled during
execution.
Section 250.5: AsyncTask: Serial Execution and Parallel
Execution of Task
AsyncTask is an abstract Class and does not inherit the Thread class. It has an abstract method
doInBackground(Params... params), which is overridden to perform the task. This method is called from
AsyncTask.call().
GoalKicker.com – Android™ Notes for Professionals 1196
Executor are part of java.util.concurrent package.
Moreover, AsyncTask contains 2 Executors
THREAD_POOL_EXECUTOR
It uses worker threads to execute the tasks parallelly.
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
SERIAL_EXECUTOR
It executes the task serially, i.e. one by one.
private static class SerialExecutor implements Executor { }
Both Executors are static, hence only one THREAD_POOL_EXECUTOR and one SerialExecutor objects exist, but you
can create several AsyncTask objects.
Therefore, if you try to do multiple background task with the default Executor (SerialExecutor), these task will be
queue and executed serially.
If you try to do multiple background task with THREAD_POOL_EXECUTOR, then they will be executed parallelly.
Example:
public class MainActivity extends Activity {
private Button bt;
private int CountTask = 0;
private static final String TAG = "AsyncTaskExample";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bt = (Button) findViewById(R.id.button);
bt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
BackgroundTask backgroundTask = new BackgroundTask ();
Integer data[] = { ++CountTask, null, null };
// Task Executed in thread pool ( 1 )
backgroundTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, data);
// Task executed Serially ( 2 )
// Uncomment the below code and comment the above code of Thread
// pool Executor and check
// backgroundTask.execute(data);
Log.d(TAG, "Task = " + (int) CountTask + " Task Queued");
}
});
}
private class BackgroundTask extends AsyncTask<Integer, Integer, Integer> {
int taskNumber;
GoalKicker.com – Android™ Notes for Professionals 1197
@Override
protected Integer doInBackground(Integer... integers) {
taskNumber = integers[0];
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Log.d(TAG, "Task = " + taskNumber + " Task Running in Background");
publishProgress(taskNumber);
return null;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected void onPostExecute(Integer aLong) {
super.onPostExecute(aLong);
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
Log.d(TAG, "Task = " + (int) values[0]
+ " Task Execution Completed");
}
}
}
Perform Click on button several times to start a task and see the result.
Task Executed in thread pool(1)
Each task takes 1000 ms to complete.
At t=36s, tasks 2, 3 and 4 are queued and started executing also because they are executing parallelly.
08-02 19:48:35.815: D/AsyncTaskExample(11693): Task = 1 Task Queued
08-02 19:48:35.815: D/AsyncTaskExample(11693): Task = 1 Task Running in Background
08-02 19:48:**36.025**: D/AsyncTaskExample(11693): Task = 2 Task Queued
08-02 19:48:**36.025**: D/AsyncTaskExample(11693): Task = 2 Task Running in Background
08-02 19:48:**36.165**: D/AsyncTaskExample(11693): Task = 3 Task Queued
08-02 19:48:**36.165**: D/AsyncTaskExample(11693): Task = 3 Task Running in Background
08-02 19:48:**36.325**: D/AsyncTaskExample(11693): Task = 4 Task Queued
08-02 19:48:**36.325**: D/AsyncTaskExample(11693): Task = 4 Task Running in Background
08-02 19:48:**36.815**: D/AsyncTaskExample(11693): Task = 1 Task Execution Completed
08-02 19:48:**36.915**: D/AsyncTaskExample(11693): Task = 5 Task Queued
08-02 19:48:**36.915**: D/AsyncTaskExample(11693): Task = 5 Task Running in Background
08-02 19:48:37.025: D/AsyncTaskExample(11693): Task = 2 Task Execution Completed
08-02 19:48:37.165: D/AsyncTaskExample(11693): Task = 3 Task Execution Completed
----------
Comment Task Executed in thread pool (1) and uncomment Task executed Serially (2).
GoalKicker.com – Android™ Notes for Professionals 1198
Perform Click on button several times to start a task and see the result.
It is executing the task serially hence every task is started after the current task completed execution. Hence when
Task 1's execution completes, only Task 2 starts running in background. Vice versa.
08-02 19:42:57.505: D/AsyncTaskExample(10299): Task = 1 Task Queued
08-02 19:42:57.505: D/AsyncTaskExample(10299): Task = 1 Task Running in Background
08-02 19:42:57.675: D/AsyncTaskExample(10299): Task = 2 Task Queued
08-02 19:42:57.835: D/AsyncTaskExample(10299): Task = 3 Task Queued
08-02 19:42:58.005: D/AsyncTaskExample(10299): Task = 4 Task Queued
08-02 19:42:58.155: D/AsyncTaskExample(10299): Task = 5 Task Queued
08-02 19:42:58.505: D/AsyncTaskExample(10299): Task = 1 Task Execution Completed
08-02 19:42:58.505: D/AsyncTaskExample(10299): Task = 2 Task Running in Background
08-02 19:42:58.755: D/AsyncTaskExample(10299): Task = 6 Task Queued
08-02 19:42:59.295: D/AsyncTaskExample(10299): Task = 7 Task Queued
08-02 19:42:59.505: D/AsyncTaskExample(10299): Task = 2 Task Execution Completed
08-02 19:42:59.505: D/AsyncTaskExample(10299): Task = 3 Task Running in Background
08-02 19:43:00.035: D/AsyncTaskExample(10299): Task = 8 Task Queued
08-02 19:43:00.505: D/AsyncTaskExample(10299): Task = 3 Task Execution Completed
08-02 19:43:**00.505**: D/AsyncTaskExample(10299): Task = 4 Task Running in Background
08-02 19:43:**01.505**: D/AsyncTaskExample(10299): Task = 4 Task Execution Completed
08-02 19:43:**01.515**: D/AsyncTaskExample(10299): Task = 5 Task Running in Background
08-02 19:43:**02.515**: D/AsyncTaskExample(10299): Task = 5 Task Execution Completed
08-02 19:43:**02.515**: D/AsyncTaskExample(10299): Task = 6 Task Running in Background
08-02 19:43:**03.515**: D/AsyncTaskExample(10299): Task = 7 Task Running in Background
08-02 19:43:**03.515**: D/AsyncTaskExample(10299): Task = 6 Task Execution Completed
08-02 19:43:04.515: D/AsyncTaskExample(10299): Task = 8 Task Running in Background
08-02 19:43:**04.515**: D/AsyncTaskExample(10299): Task = 7 Task Execution Completed
Section 250.6: Order of execution
When first introduced, AsyncTasks were executed serially on a single background thread. Starting with DONUT, this
was changed to a pool of threads allowing multiple tasks to operate in parallel. Starting with HONEYCOMB, tasks are
executed on a single thread to avoid common application errors caused by parallel execution.
If you truly want parallel execution, you can invoke executeOnExecutor(java.util.concurrent.Executor,
Object[]) with THREAD_POOL_EXECUTOR.
SERIAL_EXECUTOR -> An Executor that executes tasks one at a time in serial order.
THREAD_POOL_EXECUTOR -> An Executor that can be used to execute tasks in parallel.
sample :
Task task = new Task();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, data);
else
task.execute(data);
Section 250.7: Publishing progress
Sometimes, we need to update the progress of the computation done by an AsyncTask. This progress could be
represented by a string, an integer, etc. To do this, we have to use two functions. First, we need to set the
onProgressUpdate function whose parameter type is the same as the second type parameter of our AsyncTask.
GoalKicker.com – Android™ Notes for Professionals 1199
class YourAsyncTask extends AsyncTask<URL, Integer, Long> {
@Override
protected void onProgressUpdate(Integer... args) {
setProgressPercent(args[0])
}
}
Second, we have to use the function publishProgress necessarily on the doInBackground function, and that is all,
the previous method will do all the job.
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
}
return totalSize;
}
GoalKicker.com – Android™ Notes for Professionals 1200
Chapter 251: Testing UI with Espresso
Section 251.1: Overall Espresso
Setup Espresso :
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
androidTestCompile 'com.android.support.test:runner:0.5'
ViewMatchers – A collection of objects that implement Matcher<? super View> interface. You can pass one or
more of these to the onView method to locate a view within the current view hierarchy.
ViewActions – A collection of ViewActions that can be passed to the ViewInteraction.perform() method (for
example, click()).
ViewAssertions – A collection of ViewAssertions that can be passed the ViewInteraction.check() method. Most
of the time, you will use the matches assertion, which uses a View matcher to assert the state of the currently
selected view.
Espresso cheat sheet by google
GoalKicker.com – Android™ Notes for Professionals 1201
GoalKicker.com – Android™ Notes for Professionals 1202
Enter Text In EditText
onView(withId(R.id.edt_name)).perform(typeText("XYZ"));
closeSoftKeyboard();
Perform Click on View
onView(withId(R.id.btn_id)).perform(click());
Checking View is Displayed
onView(withId(R.id.edt_pan_number)).check(ViewAssertions.matches((isDisplayed())));
Section 251.2: Espresso simple UI test
UI testing tools
Two main tools that are nowadays mostly used for UI testing are Appium and Espresso.
Appium Espresso
blackbox test white/gray box testing
what you see is what you can test
can change inner workings of the app and prepare it for testing,
e.g. save some data to database or sharedpreferences before
running the test
used mostly for integration end to end tests
and entire user flows testing the functionality of a screen and/or flow
can be abstracted so test written can be
executed on iOS and Android Android Only
well supported well supported
supports parallel testing on multiple devices
with selenium grid
Not out of the box parallel testing, plugins like Spoon exists until
true Google support comes out
How to add espresso to the project
dependencies {
// Set this dependency so you can use Android JUnit Runner
androidTestCompile 'com.android.support.test:runner:0.5'
// Set this dependency to use JUnit 4 rules
androidTestCompile 'com.android.support.test:rules:0.5'
// Set this dependency to build and run Espresso tests
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
// Set this dependency to build and run UI Automator tests
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.2.2'
}
NOTE If you are using latest support libraries, annotations etc. you need to exclude the older versions from
espresso to avoid collisions:
// there is a conflict with the test support library (see
http://stackoverflow.com/questions/29857695)
// so for now re exclude the support-annotations dependency from here to avoid clashes
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2') {
exclude group: 'com.android.support', module: 'support-annotations'
exclude module: 'support-annotations'
exclude module: 'recyclerview-v7'
exclude module: 'support-v4'
exclude module: 'support-v7'
}
GoalKicker.com – Android™ Notes for Professionals 1203
// exclude a couple of more modules here because of <http://stackoverflow.com/questions/29216327>
and
// more specifically of <https://code.google.com/p/android-test-kit/issues/detail?id=139>
// otherwise you'll receive weird crashes on devices and dex exceptions on emulators
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks,
CountingIdlingResource
androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.2.2') {
exclude group: 'com.android.support', module: 'support-annotations'
exclude group: 'com.android.support', module: 'design'
exclude module: 'support-annotations'
exclude module: 'recyclerview-v7'
exclude module: 'support-v4'
exclude module: 'support-v7'
}
//excluded specific packages due to https://code.google.com/p/android/issues/detail?id=183454
androidTestCompile('com.android.support.test.espresso:espresso-intents:2.2.2') {
exclude group: 'com.android.support', module: 'support-annotations'
exclude module: 'support-annotations'
exclude module: 'recyclerview-v7'
exclude module: 'support-v4'
exclude module: 'support-v7'
}
androidTestCompile('com.android.support.test.espresso:espresso-web:2.2.2') {
exclude group: 'com.android.support', module: 'support-annotations'
exclude module: 'support-annotations'
exclude module: 'recyclerview-v7'
exclude module: 'support-v4'
exclude module: 'support-v7'
}
androidTestCompile('com.android.support.test:runner:0.5') {
exclude group: 'com.android.support', module: 'support-annotations'
exclude module: 'support-annotations'
exclude module: 'recyclerview-v7'
exclude module: 'support-v4'
exclude module: 'support-v7'
}
androidTestCompile('com.android.support.test:rules:0.5') {
exclude group: 'com.android.support', module: 'support-annotations'
exclude module: 'support-annotations'
exclude module: 'recyclerview-v7'
exclude module: 'support-v4'
exclude module: 'support-v7'
}
Other than these imports it is necessary to add android instrumentation test runner to build.gradle
android.defaultConfig:
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
Device setup
For non flaky test it is recommended to set following settings on your devices:
Developer options / Disable Animations - reduces flakyness of tests
Developer options / Stay awake - if you have dedicated devices for tests this is usefull
Developer options / Logger buffer sizes - set to higher number if you run very big test suites on your phone
Accessibility / Touch & Hold delay - long to avoid problems with tapping in espresso
GoalKicker.com – Android™ Notes for Professionals 1204
Quite a setup from the real world ha? Well now when thats out of the way lets take a look how to setup a small test
Writing the test
Lets assume that we have the following screen:
The screen contains:
text input field - R.id.textEntry
button which shows snackbar with typed text when clicked - R.id.shownSnackbarBtn
snackbar which should contain user typed text - android.support.design.R.id.snackbar_text
Now lets create a class that will test our flow:
/**
* Testing of the snackbar activity.
**/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class SnackbarActivityTest{
//espresso rule which tells which activity to start
@Rule
public final ActivityTestRule<SnackbarActivity> mActivityRule =
new ActivityTestRule<>(SnackbarActivity.class, true, false);
@Override
public void tearDown() throws Exception {
super.tearDown();
//just an example how tear down should cleanup after itself
GoalKicker.com – Android™ Notes for Professionals 1205
mDatabase.clear();
mSharedPrefs.clear();
}
@Override
public void setUp() throws Exception {
super.setUp();
//setting up your application, for example if you need to have a user in shared
//preferences to stay logged in you can do that for all tests in your setup
User mUser = new User();
mUser.setToken("randomToken");
}
/**
*Test methods should always start with "testXYZ" and it is a good idea to
*name them after the intent what you want to test
**/
@Test
public void testSnackbarIsShown() {
//start our activity
mActivityRule.launchActivity(null);
//check is our text entry displayed and enter some text to it
String textToType="new snackbar text";
onView(withId(R.id.textEntry)).check(matches(isDisplayed()));
onView(withId(R.id.textEntry)).perform(typeText(textToType));
//click the button to show the snackbar
onView(withId(R.id.shownSnackbarBtn)).perform(click());
//assert that a view with snackbar_id with text which we typed and is displayed
onView(allOf(withId(android.support.design.R.id.snackbar_text),
withText(textToType))) .check(matches(isDisplayed()));
}
}
As you noticed there are 3-4 things that you might notice come often:
onView(withXYZ) <-- viewMatchers with them you are able to find elements on screen
perform(click()) <-- viewActions, you can execute actions on elements you previously found
check(matches(isDisplayed())) <-- viewAssertions, checks you want to do on screens you previously found
All of these and many others can be found here:
https://google.github.io/android-testing-support-library/docs/espresso/cheatsheet/index.html
Thats it, now you can run the test either with right clicking on the class name / test and selecting Run test or with
command:
./gradlew connectedFLAVORNAMEAndroidTest
Section 251.3: Open Close DrawerLayout
public final class DrawerLayoutTest {
@Test public void Open_Close_Drawer_Layout() {
onView(withId(R.id.drawer_layout)).perform(actionOpenDrawer());
onView(withId(R.id.drawer_layout)).perform(actionCloseDrawer());
}
public static ViewAction actionOpenDrawer() {
GoalKicker.com – Android™ Notes for Professionals 1206
return new ViewAction() {
@Override public Matcher<View> getConstraints() {
return isAssignableFrom(DrawerLayout.class);
}
@Override public String getDescription() {
return "open drawer";
}
@Override public void perform(UiController uiController, View view) {
((DrawerLayout) view).openDrawer(GravityCompat.START);
}
};
}
public static ViewAction actionCloseDrawer() {
return new ViewAction() {
@Override public Matcher<View> getConstraints() {
return isAssignableFrom(DrawerLayout.class);
}
@Override public String getDescription() {
return "close drawer";
}
@Override public void perform(UiController uiController, View view) {
((DrawerLayout) view).closeDrawer(GravityCompat.START);
}
};
}
}
Section 251.4: Set Up Espresso
In the build.gradle file of your Android app module add next dependencies:
dependencies {
// Android JUnit Runner
androidTestCompile 'com.android.support.test:runner:0.5'
// JUnit4 Rules
androidTestCompile 'com.android.support.test:rules:0.5'
// Espresso core
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks,
CountingIdlingResource
androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2.2'
//UI Automator tests
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.2.2'
}
Specify the AndroidJUnitRunner for the testInstrumentationRunner parameter in the build.gradle file.
android {
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
GoalKicker.com – Android™ Notes for Professionals 1207
Additionally, add this dependency for providing intent mocking support
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.2'
And add this one for webview testing support
// Espresso-web for WebView support
androidTestCompile 'com.android.support.test.espresso:espresso-web:2.2.2'
Section 251.5: Performing an action on a view
It is possible to perform ViewActions on a view using the perform method.
The ViewActions class provides helper methods for the most common actions, like:
ViewActions.click()
ViewActions.typeText()
ViewActions.clearText()
For example, to click on the view:
onView(...).perform(click());
onView(withId(R.id.button_simple)).perform(click());
You can execute more than one action with one perform call:
onView(...).perform(typeText("Hello"), click());
If the view you are working with is located inside a ScrollView (vertical or horizontal), consider preceding actions
that require the view to be displayed (like click() and typeText()) with scrollTo(). This ensures that the view is
displayed before proceeding to the other action:
onView(...).perform(scrollTo(), click());
Section 251.6: Finding a view with onView
With the ViewMatchers you can find view in the current view hierarchy.
To find a view, use the onView() method with a view matcher which selects the correct view. The onView() methods
return an object of type ViewInteraction.
For example, finding a view by its R.id is as simple as:
onView(withId(R.id.my_view))
Finding a view with a text:
onView(withText("Hello World"))
Section 251.7: Create Espresso Test Class
Place next java class in src/androidTest/java and run it.
public class UITest {
GoalKicker.com – Android™ Notes for Professionals 1208
@Test public void Simple_Test() {
onView(withId(R.id.my_view)) // withId(R.id.my_view) is a ViewMatcher
.perform(click()) // click() is a ViewAction
.check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
}
}
Section 251.8: Up Navigation
@Test
public void testUpNavigation() {
intending(hasComponent(ParentActivity.class.getName())).respondWith(new
Instrumentation.ActivityResult(0, null));
onView(withContentDescription("Navigate up")).perform(click());
intended(hasComponent(ParentActivity.class.getName()));
}
Note that this is a workaround and will collide with other Views that have the same content description.
Section 251.9: Group a collection of test classes in a test suite
You can organize the execution of your instrumented unit tests defining a Suite.
/**
* Runs all unit tests.
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({MyTest1.class ,
MyTest2.class,
MyTest3.class})
public class AndroidTestSuite {}
Then in AndroidStudio you can run with gradle or setting a new configuration like:
GoalKicker.com – Android™ Notes for Professionals 1209
Test suites can be nested.
Section 251.10: Espresso custom matchers
Espresso by default has many matchers that help you find views that you need to do some checks or interactions
with them.
Most important ones can be found in the following cheat sheet:
https://google.github.io/android-testing-support-library/docs/espresso/cheatsheet/
Some examples of matchers are:
withId(R.id.ID_of_object_you_are_looking_for);
withText("Some text you expect object to have");
isDisplayed() <-- check is the view visible
doesNotExist() <-- check that the view does not exist
All of these are very useful for everyday use, but if you have more complex views writing your custom matchers can
make the tests more readable and you can reuse them in different places.
There are 2 most common type of matchers you can extend: TypeSafeMatcher BoundedMatcher
Implementing TypeSafeMatcher requires you to check the instanceOf the view you are asserting against, if its the
correct type you match some of its properties against a value you provided to a matcher.
For example, type safe matcher that validates an image view has correct drawable:
public class DrawableMatcher extends TypeSafeMatcher<View> {
private @DrawableRes final int expectedId;
String resourceName;
GoalKicker.com – Android™ Notes for Professionals 1210
public DrawableMatcher(@DrawableRes int expectedId) {
super(View.class);
this.expectedId = expectedId;
}
@Override
protected boolean matchesSafely(View target) {
//Type check we need to do in TypeSafeMatcher
if (!(target instanceof ImageView)) {
return false;
}
//We fetch the image view from the focused view
ImageView imageView = (ImageView) target;
if (expectedId < 0) {
return imageView.getDrawable() == null;
}
//We get the drawable from the resources that we are going to compare with image view source
Resources resources = target.getContext().getResources();
Drawable expectedDrawable = resources.getDrawable(expectedId);
resourceName = resources.getResourceEntryName(expectedId);
if (expectedDrawable == null) {
return false;
}
//comparing the bitmaps should give results of the matcher if they are equal
Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
Bitmap otherBitmap = ((BitmapDrawable) expectedDrawable).getBitmap();
return bitmap.sameAs(otherBitmap);
}
@Override
public void describeTo(Description description) {
description.appendText("with drawable from resource id: ");
description.appendValue(expectedId);
if (resourceName != null) {
description.appendText("[");
description.appendText(resourceName);
description.appendText("]");
}
}
}
Usage of the matcher could be wrapped like this:
public static Matcher<View> withDrawable(final int resourceId) {
return new DrawableMatcher(resourceId);
}
onView(withDrawable(R.drawable.someDrawable)).check(matches(isDisplayed()));
Bounded matchers are similar you just don't have to do the type check but, since that is done automagically for
you:
/**
* Matches a {@link TextInputFormView}'s input hint with the given resource ID
*
* @param stringId
* @return
*/
GoalKicker.com – Android™ Notes for Professionals 1211
public static Matcher<View> withTextInputHint(@StringRes final int stringId) {
return new BoundedMatcher<View, TextInputFormView>(TextInputFormView.class) {
private String mResourceName = null;
@Override
public void describeTo(final Description description) {
//fill these out properly so your logging and error reporting is more clear
description.appendText("with TextInputFormView that has hint ");
description.appendValue(stringId);
if (null != mResourceName) {
description.appendText("[");
description.appendText(mResourceName);
description.appendText("]");
}
}
@Override
public boolean matchesSafely(final TextInputFormView view) {
if (null == mResourceName) {
try {
mResourceName = view.getResources().getResourceEntryName(stringId);
} catch (Resources.NotFoundException e) {
throw new IllegalStateException("could not find string with ID " + stringId,
e);
}
}
return view.getResources().getString(stringId).equals(view.getHint());
}
};
}
More on matchers can be read up on:
http://hamcrest.org/
https://developer.android.com/reference/android/support/test/espresso/matcher/ViewMatchers.html
GoalKicker.com – Android™ Notes for Professionals 1212
Chapter 252: Writing UI tests - Android
Focus of this document is to represent goals and ways how to write android UI and integration tests. Espresso and
UIAutomator are provided by Google so focus should be around these tools and their respective wrappers e.g.
Appium, Spoon etc.
Section 252.1: MockWebServer example
In case your activities, fragments and UI require some background processing a good thing to use is a
MockWebServer which runs localy on an android device which brings a closed and testable enviroment for your UI.
https://github.com/square/okhttp/tree/master/mockwebserver
First step is including the gradle dependency:
testCompile 'com.squareup.okhttp3:mockwebserver:(insert latest version)'
Now steps for running and using the mock server are:
create mock server object
start it at specific address and port (usually localhost:portnumber)
enqueue responses for specific requests
start the test
This is nicely explained in the github page of the mockwebserver but in our case we want something nicer and
reusable for all tests, and JUnit rules will come nicely into play here:
/**
*JUnit rule that starts and stops a mock web server for test runner
*/
public class MockServerRule extends UiThreadTestRule {
private MockWebServer mServer;
public static final int MOCK_WEBSERVER_PORT = 8000;
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
startServer();
try {
base.evaluate();
} finally {
stopServer();
}
}
};
}
/**
* Returns the started web server instance
*
* @return mock server
*/
public MockWebServer server() {
GoalKicker.com – Android™ Notes for Professionals 1213
return mServer;
}
public void startServer() throws IOException, NoSuchAlgorithmException {
mServer = new MockWebServer();
try {
mServer(MOCK_WEBSERVER_PORT);
} catch (IOException e) {
throw new IllegalStateException(e,"mock server start issue");
}
}
public void stopServer() {
try {
mServer.shutdown();
} catch (IOException e) {
Timber.e(e, "mock server shutdown error”);
}
}
}
Now lets assume that we have the exact same activity like in previous example, just in this case when we push the
button app will fetch something from the network for example: https://someapi.com/name
This would return some text string which would be concatenated in the snackbar text e.g. NAME + text you typed in.
/**
* Testing of the snackbar activity with networking.
**/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class SnackbarActivityTest{
//espresso rule which tells which activity to start
@Rule
public final ActivityTestRule<SnackbarActivity> mActivityRule =
new ActivityTestRule<>(SnackbarActivity.class, true, false);
//start mock web server
@Rule
public final MockServerRule mMockServerRule = new MockServerRule();
@Override
public void tearDown() throws Exception {
//same as previous example
}
@Override
public void setUp() throws Exception {
//same as previous example
**//IMPORTANT:** point your application to your mockwebserver endpoint e.g.
MyAppConfig.setEndpointURL("http://localhost:8000");
}
/**
*Test methods should always start with "testXYZ" and it is a good idea to
*name them after the intent what you want to test
**/
@Test
public void testSnackbarIsShown() {
//setup mockweb server
GoalKicker.com – Android™ Notes for Professionals 1214
mMockServerRule.server().setDispatcher(getDispatcher());
mActivityRule.launchActivity(null);
//check is our text entry displayed and enter some text to it
String textToType="new snackbar text";
onView(withId(R.id.textEntry)).check(matches(isDisplayed()));
//we check is our snackbar showing text from mock webserver plus the one we typed
onView(withId(R.id.textEntry)).perform(typeText("JazzJackTheRabbit" + textToType));
//click the button to show the snackbar
onView(withId(R.id.shownSnackbarBtn)).perform(click());
//assert that a view with snackbar_id with text which we typed and is displayed
onView(allOf(withId(android.support.design.R.id.snackbar_text),
withText(textToType))) .check(matches(isDisplayed()));
}
/**
*creates a mock web server dispatcher with prerecorded requests and responses
**/
private Dispatcher getDispatcher() {
final Dispatcher dispatcher = new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
if (request.getPath().equals("/name")){
return new MockResponse().setResponseCode(200)
.setBody("JazzJackTheRabbit");
}
throw new IllegalStateException("no mock set up for " + request.getPath());
}
};
return dispatcher;
}
I would suggest wrapping the dispatcher in some sort of a Builder so you can easily chain and add new responses
for your screens. e.g.
return newDispatcherBuilder()
.withSerializedJSONBody("/authenticate", Mocks.getAuthenticationResponse())
.withSerializedJSONBody("/getUserInfo", Mocks.getUserInfo())
.withSerializedJSONBody("/checkNotBot", Mocks.checkNotBot());
Section 252.2: IdlingResource
The power of idling resources lies in not having to wait for some app's processing (networking, calculations,
animations, etc.) to finish with sleep(), which brings flakiness and/or prolongs the tests run. The official
documentation can be found here.
Implementation
There are three things that you need to do when implementing IdlingResource interface:
getName() - Returns the name of your idling resource.
isIdleNow() - Checks whether your xyz object, operation, etc. is idle at the moment.
registerIdleTransitionCallback (IdlingResource.ResourceCallback callback) - Provides a callback which
you should call when your object transitions to idle.
Now you should create your own logic and determine when your app is idle and when not, since this is dependant
on the app. Below you will find a simple example, just to show how it works. There are other examples online, but
specific app implementation brings to specific idling resource implementations.
GoalKicker.com – Android™ Notes for Professionals 1215
NOTES
There have been some Google examples where they put IdlingResources in the code of the app. Do not do
this. They presumably placed it there just to show how they work.
Keeping your code clean and maintaining single principle of responsibility is up to you!
Example
Let us say that you have an activity which does weird stuff and takes a long time for the fragment to load and thus
making your Espresso tests fail by not being able to find resources from your fragment (you should change how
your activity is created and when to speed it up). But in any case to keep it simple, the following example shows
how it should look like.
Our example idling resource would get two objects:
The tag of the fragment which you need to find and waiting to get attached to the activity.
A FragmentManager object which is used for finding the fragment.
/**
* FragmentIdlingResource - idling resource which waits while Fragment has not been loaded.
*/
public class FragmentIdlingResource implements IdlingResource {
private final FragmentManager mFragmentManager;
private final String mTag;
//resource callback you use when your activity transitions to idle
private volatile ResourceCallback resourceCallback;
public FragmentIdlingResource(FragmentManager fragmentManager, String tag) {
mFragmentManager = fragmentManager;
mTag = tag;
}
@Override
public String getName() {
return FragmentIdlingResource.class.getName() + ":" + mTag;
}
@Override
public boolean isIdleNow() {
//simple check, if your fragment is added, then your app has became idle
boolean idle = (mFragmentManager.findFragmentByTag(mTag) != null);
if (idle) {
//IMPORTANT: make sure you call onTransitionToIdle
resourceCallback.onTransitionToIdle();
}
return idle;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
}
Now that you have your IdlingResource written, you need to use it somewhere right?
Usage
Let us skip the entire test class setup and just look how a test case would look like:
GoalKicker.com – Android™ Notes for Professionals 1216
@Test
public void testSomeFragmentText() {
mActivityTestRule.launchActivity(null);
//creating the idling resource
IdlingResource fragmentLoadedIdlingResource = new
FragmentIdlingResource(mActivityTestRule.getActivity().getSupportFragmentManager(),
SomeFragmentText.TAG);
//registering the idling resource so espresso waits for it
Espresso.registerIdlingResources(idlingResource1);
onView(withId(R.id.txtHelloWorld)).check(matches(withText(helloWorldText)));
//lets cleanup after ourselves
Espresso.unregisterIdlingResources(fragmentLoadedIdlingResource);
}
Combination with JUnit rule
This is not to hard; you can also apply the idling resource in form of a JUnit test rule. For example, let us say that
you have some SDK that contains Volley in it and you want Espresso to wait for it. Instead of going through each
test case or applying it in setup, you could create a JUnit rule and just write:
@Rule
public final SDKIdlingRule mSdkIdlingRule = new SDKIdlingRule(SDKInstanceHolder.getInstance());
Now since this is an example, don't take it for granted; all code here is imaginary and used only for demonstration
purposes:
public class SDKIdlingRule implements TestRule {
//idling resource you wrote to check is volley idle or not
private VolleyIdlingResource mVolleyIdlingResource;
//request queue that you need from volley to give it to idling resource
private RequestQueue mRequestQueue;
//when using the rule extract the request queue from your SDK
public SDKIdlingRule(SDKClass sdkClass) {
mRequestQueue = getVolleyRequestQueue(sdkClass);
}
private RequestQueue getVolleyRequestQueue(SDKClass sdkClass) {
return sdkClass.getVolleyRequestQueue();
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
//registering idling resource
mVolleyIdlingResource = new VolleyIdlingResource(mRequestQueue);
Espresso.registerIdlingResources(mVolleyIdlingResource);
try {
base.evaluate();
} finally {
if (mVolleyIdlingResource != null) {
//deregister the resource when test finishes
Espresso.unregisterIdlingResources(mVolleyIdlingResource);
}
}
}
};
GoalKicker.com – Android™ Notes for Professionals 1217
}
}
GoalKicker.com – Android™ Notes for Professionals 1218
Chapter 253: Unit testing in Android with
JUnit
Section 253.1: Moving Business Logic Out of Android
Componenets
A lot of the value from local JVM unit tests comes from the way you design your application. You have to design it in
such a way where you can decouple your business logic from your Android Components. Here is an example of
such a way using the Model-View-Presenter pattern. Lets practice this out by implementing a basic sign up screen
that only takes a username and password. Our Android app is responsible for validating that the username the
user supplies is not blank and that the password is at least eight characters long and contains at least one digit. If
the username/password is valid we perform our sign up api call, otherwise we display an error message.
Example where business logic is highly coupled with Android Component.
public class LoginActivity extends Activity{
...
private void onSubmitButtonClicked(){
String username = findViewById(R.id.username).getText().toString();
String password = findViewById(R.id.password).getText().toString();
boolean isUsernameValid = username != null && username.trim().length() != 0;
boolean isPasswordValid = password != null && password.trim().length() >= 8 &&
password.matches(".*\\d+.*");
if(isUsernameValid && isPasswordValid){
performSignUpApiCall(username, password);
} else {
displayInvalidCredentialsErrorMessage();
}
}
}
Example where business logic is decoupled from Android Component.
Here we define in a single class, LoginContract, that will house the various interactions between our various classes.
public interface LoginContract {
public interface View {
performSignUpApiCall(String username, String password);
displayInvalidCredentialsErrorMessage();
}
public interface Presenter {
void validateUserCredentials(String username, String password);
}
}
Our LoginActivity is for the most part the same except that we have removed the responsibility of having to know
how to validate a user's sign up form (our business logic). The LoginActivity will now rely on our new LoginPresenter
to perform validation.
public class LoginActivity extends Activity implements LoginContract.View{
private LoginContract.Presenter presenter;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
presenter = new LoginPresenter(this);
....
GoalKicker.com – Android™ Notes for Professionals 1219
}
...
private void onSubmitButtonClicked(){
String username = findViewById(R.id.username).getText().toString();
String password = findViewById(R.id.password).getText().toString();
presenter.validateUserCredentials(username, password);
}
...
}
Now your business logic will reside in your new LoginPresenter class.
public class LoginPresenter implements LoginContract.Presenter{
private LoginContract.View view;
public LoginPresenter(LoginContract.View view){
this.view = view;
}
public void validateUserCredentials(String username, String password){
boolean isUsernameValid = username != null && username.trim().length() != 0;
boolean isPasswordValid = password != null && password.trim().length() >= 8 &&
password.matches(".*\\d+.*");
if(isUsernameValid && isPasswordValid){
view.performSignUpApiCall(username, password);
} else {
view.displayInvalidCredentialsErrorMessage();
}
}
}
And now we can create local JVM unit tests against your new LoginPresenter class.
public class LoginPresenterTest {
@Mock
LoginContract.View view;
private LoginPresenter presenter;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
presenter = new LoginPresenter(view);
}
@Test
public void test_validateUserCredentials_userDidNotEnterUsername_displayErrorMessage() throws
Exception {
String username = "";
String password = "kingslayer1";
presenter.validateUserCredentials(username, password);
Mockito.verify(view). displayInvalidCredentialsErrorMessage();
}
@Test
public void
test_validateUserCredentials_userEnteredFourLettersAndOneDigitPassword_displayErrorMessage() throws
Exception {
String username = "Jaime Lanninster";
GoalKicker.com – Android™ Notes for Professionals 1220
String password = "king1";
presenter.validateUserCredentials(username, password);
Mockito.verify(view). displayInvalidCredentialsErrorMessage();
}
@Test
public void
test_validateUserCredentials_userEnteredNineLettersButNoDigitsPassword_displayErrorMessage() throws
Exception {
String username = "Jaime Lanninster";
String password = "kingslayer";
presenter.validateUserCredentials(username, password);
Mockito.verify(view). displayInvalidCredentialsErrorMessage();
}
@Test
public void
test_validateUserCredentials_userEnteredNineLettersButOneDigitPassword_performApiCallToSignUpUser()
throws Exception {
String username = "Jaime Lanninster";
String password = "kingslayer1";
presenter.validateUserCredentials(username, password);
Mockito.verify(view).performSignUpApiCall(username, password);
}
}
As you can see, when we extracted our business logic out of the LoginActivity and placed it in the LoginPresenter
POJO. We can now create local JVM unit tests against our business logic.
It should be noted that there are various other implications from our change in architecture such as we are close to
adhering to each class having a single responsibility, additional classes, etc. These are just side effects of the way I
choose to go about performing this decoupling via the MVP style. MVP is just one way to go about this but there are
other alternatives that you may want to look at such as MVVM. You just have to pick the best system that works for
you.
Section 253.2: Creating Local unit tests
Place your test classes here: /src/test/<pkg_name>/
Example test class
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
int a=4, b=5, c;
c = a + b;
assertEquals(9, c); // This test passes
assertEquals(10, c); //Test fails
}
}
Breakdown
public class ExampleUnitTest {
...
}
The test class, you can create several test classes and place them inside the test package.
@Test
GoalKicker.com – Android™ Notes for Professionals 1221
public void addition_isCorrect() {
...
}
The test method, several test methods can be created inside a test class.
Notice the annotation @Test.
The Test annotation tells JUnit that the public void method to which it is attached can be run as a test
case.
There are several other useful annotations like @Before, @After etc. This page would be a good place to start.
assertEquals(9, c); // This test passes
assertEquals(10, c); //Test fails
These methods are member of the Assert class. Some other useful methods are assertFalse(), assertNotNull(),
assertTrue etc. Here's an elaborate Explanation.
Annotation Information for JUnit Test:
@Test: The Test annotation tells JUnit that the public void method to which it is attached can be run as a test case.
To run the method, JUnit first constructs a fresh instance of the class then invokes the annotated method.
@Before: When writing tests, it is common to find that several tests need similar objects created before they can
run. Annotating a public void method with @Before causes that method to be run before the Test method.
@After: If you allocate external resources in a Before method you need to release them after the test runs.
Annotating a public void method with @After causes that method to be run after the Test method. All @After
methods are guaranteed to run even if a Before or Test method throws an exception
Tip Quickly create test classes in Android Studio
Place the cursor on the class name for which you want to create a test class.
Press Alt + Enter (Windows).
Select Create Test, hit Return.
Select the methods for which you want to create test methods, click OK.
Select the directory where you want to create the test class.
You're done, this what you get is your first test.
Tip Easily execute tests in Android Studio
Right click test the package.
Select Run 'Tests in ...
All the tests in the package will be executed at once.
Section 253.3: Getting started with JUnit
Setup
To start unit testing your Android project using JUnit you need to add the JUnit dependency to your project and you
need to create a test source-set which is going to contain the source code for the unit tests. Projects created with
Android Studio often already include the JUnit dependency and the test source-set
GoalKicker.com – Android™ Notes for Professionals 1222
Add the following line to your module build.gradle file within the dependencies Closure:
testCompile 'junit:junit:4.12'
JUnit test classes are located in a special source-set named test. If this source-set does not exist you need to create
a new folder yourself. The folder structure of a default Android Studio (Gradle based) project looks like this:
<project-root-folder>
/app (module root folder)
/build
/libs
/src
/main (source code)
/test (unit test source code)
/androidTest (instrumentation test source code)
build.gradle (module gradle file)
/build
/gradle
build.gradle (project gradle file)
gradle.properties
gradlew
gradlew.bat
local.properties
settings.gradle (gradle settings)
If your project doesn't have the /app/src/test folder you need to create it yourself. Within the test folder you also
need a java folder (create it if it doesn't exist). The java folder in the test source set should contains the same
package structure as your main source-set.
If setup correctly your project structure (in the Android view in Android Studio) should look like this:
Note: You don't necessarily need to have the androidTest source-set, this source-set is often found in projects created by
Android Studio and is included here for reference.
Writing a test
1. Create a new class within the test source-set.
Right click the test source-set in the project view choose New > Java class.
The most used naming pattern is to use the name of the class you're going to test with Test added to it. So
StringUtilities becomes StringUtilitiesTest.
2. Add the @RunWith annotation
The @RunWith annotation is needed in order to make JUnit run the tests we're going to define in our test
class. The default JUnit runner (for JUnit 4) is the BlockJUnit4ClassRunner but instead of using this running
GoalKicker.com – Android™ Notes for Professionals 1223
directly its more convenient to use the alias JUnit4 which is a shorthand for the default JUnit runner.
@RunWith(JUnit4.class)
public class StringUtilitiesTest {
}
3. Create a test
A unit test is essentially just a method which, in most cases, should not fail if run. In other words it should not
throw an exception. Inside a test method you will almost always find assertions that check if specific
conditions are met. If an assertion fails it throws an exception which causes the method/test to fail. A test
method is always annotated with the @Test annotation. Without this annotation JUnit won't automatically run
the test.
@RunWith(JUnit4.class)
public class StringUtilitiesTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals("Hello JUnit", "Hello" + " " + "JUnit");
}
}
Note: unlike the standard Java method naming convention unit test method names do often contain underscores.
Running a test
1. Method
To run a single test method you can right click the method and click Run 'addition_isCorrect()' or use the
keyboard shortcut ctrl+shift+f10.
If everything is setup correctly JUnit starts running the method and you should see the following interface
within Android Studio:
GoalKicker.com – Android™ Notes for Professionals 1224
2. Class
You can also run all the tests defined in a single class, by right clicking the class in the project view and
clicking Run 'StringUtilitiesTest ' or use the keyboard shortcut ctrl+shift+f10 if you have selected the
class in the project view.
3. Package (everything)
If you wan't to run all the tests defined in the project or in a package you can just right click the package and
click Run ... just like you would run all the tests defined in a single class.
Section 253.4: Exceptions
JUnit can also be used to test if a method throws a specific exception for a given input.
In this example we will test if the following method really throws an exception if the Boolean format (input) is not
recognized/unknown:
public static boolean parseBoolean(@NonNull String raw) throws IllegalArgumentException{
raw = raw.toLowerCase().trim();
switch (raw) {
case "t": case "yes": case "1": case "true":
return true;
case "f": case "no": case "0": case "false":
return false;
default:
throw new IllegalArgumentException("Unknown boolean format: " + raw);
}
}
By adding the expected parameter to the @Test annotation, one can define which exception is expected to be
thrown. The unit test will fail if this exception does not occur, and succeed if the exception is indeed thrown:
@Test(expected = IllegalArgumentException.class)
public void parseBoolean_parsesInvalidFormat_throwsException(){
StringUtilities.parseBoolean("Hello JUnit");
}
This works well, however, it does limit you to just a single test case within the method. Sometimes you might want
to test multiple cases within a single method. A technique often used to overcome this limitation is using try-catch
blocks and the Assert.fail() method:
@Test
public void parseBoolean_parsesInvalidFormats_throwsException(){
GoalKicker.com – Android™ Notes for Professionals 1225
try {
StringUtilities.parseBoolean("Hello!");
fail("Expected IllegalArgumentException");
} catch(IllegalArgumentException e){
}
try {
StringUtilities.parseBoolean("JUnit!");
fail("Expected IllegalArgumentException");
} catch(IllegalArgumentException e){
}
}
Note: Some people consider it to be bad practice to test more than a single case inside a unit test.
Section 253.5: Static import
JUnit defines quite some assertEquals methods at least one for each primitive type and one for Objects is
available. These methods are by default not directly available to call and should be called like this:
Assert.assertEquals. But because these methods are used so often people almost always use a static import so
that the method can be directly used as if it is part of the class itself.
To add a static import for the assertEquals method use the following import statement:
import static org.junit.Assert.assertEquals;
You can also static import all assert methods including the assertArrayEquals, assertNotNull and assertFalse
etc. using the following static import:
import static org.junit.Assert.*;
Without static import:
@Test
public void addition_isCorrect(){
Assert.assertEquals(4 , 2 + 2);
}
With static import:
@Test
public void addition_isCorrect(){
assertEquals(4 , 2 + 2);
}
GoalKicker.com – Android™ Notes for Professionals 1226
Chapter 254: Inter-app UI testing with
UIAutomator
Section 254.1: Prepare your project and write the first
UIAutomator test
Add the required libraries into the dependencies section of your Android module's build.gradle:
android {
...
defaultConfig {
...
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
dependencies {
...
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
androidTestCompile 'com.android.support:support-annotations:23.4.0'
}
􀀀 Note that of course the versions may differ in the mean time.
After this sync with the changes.
Then add a new Java class inside the androidTest folder:
public class InterAppTest extends InstrumentationTestCase {
private UiDevice device;
@Override
public void setUp() throws Exception {
device = UiDevice.getInstance(getInstrumentation());
}
public void testPressHome() throws Exception {
device.pressHome();
}
}
By making a right click on the class tab and on "Run "InterAppTest" executes this test.
Section 254.2: Writing more complex tests using the
UIAutomatorViewer
In order to enable writing more complex UI tests the UIAutomatorViewer is needed. The tool located at /tools/ makes
a fullscreen screenshot including the layouts of the currently displayed views. See the subsequent picture to get an
idea of what is shown:
GoalKicker.com – Android™ Notes for Professionals 1227
For the UI tests we are looking for resource-id, content-desc or something else to identify a view and use it inside our
tests.
The uiautomatorviewer is executed via terminal.
If we now for instance want to click on the applications button and then open some app and swipe around, this is
how the test method can look like:
public void testOpenMyApp() throws Exception {
// wake up your device
device.wakeUp();
// switch to launcher (hide the previous application, if some is opened)
device.pressHome();
// enter applications menu (timeout=200ms)
device.wait(Until.hasObject(By.desc(("Apps"))), 200);
UiObject2 appsButton = device.findObject(By.desc(("Apps")));
assertNotNull(appsButton);
appsButton.click();
// enter some application (timeout=200ms)
device.wait(Until.hasObject(By.desc(("MyApplication"))), 200);
UiObject2 someAppIcon = device.findObject(By.desc(("MyApplication")));
assertNotNull(someAppIcon);
someAppIcon.click();
// do a swipe (steps=20 is 0.1 sec.)
device.swipe(200, 1200, 1300, 1200, 20);
assertTrue(isSomeConditionTrue)
}
GoalKicker.com – Android™ Notes for Professionals 1228
Section 254.3: Creating a test suite of UIAutomator tests
Putting UIAutomator tests together to a test suite is a quick thing:
package de.androidtest.myapplication;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({InterAppTest1.class, InterAppTest2.class})
public class AppTestSuite {}
Execute similar to a single test by clicking right and run the suite.
GoalKicker.com – Android™ Notes for Professionals 1229
Chapter 255: Lint Warnings
Section 255.1: Using tools:ignore in xml files
The attribute tools:ignore can be used in xml files to dismiss lint warnings.
BUT dismissing lint warnings with this technique is most of the time the wrong way to proceed.
A lint warning must be understood and fixed... it can be ignored if and only if you have a full understanding of it's
meaning and a strong reason to ignore it.
Here is a use case where it legitimate to ignore a lint warning:
You are developing a system-app (signed with the device manufacturer key)
Your app need to change the device date (or any other protected action)
Then you can do this in your manifest : (i.e. requesting the protected permission and ignoring the lint warning
because you know that in your case the permission will be granted)
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
...>
<uses-permission android:name="android.permission.SET_TIME"
tools:ignore="ProtectedPermissions"/>
Section 255.2: Configure LintOptions with gradle
You can configure lint by adding a lintOptions section in the build.gradle file:
android {
//.....
lintOptions {
// turn off checking the given issue id's
disable 'TypographyFractions','TypographyQuotes'
// turn on the given issue id's
enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
// check *only* the given issue id's
check 'NewApi', 'InlinedApi'
// set to true to turn off analysis progress reporting by lint
quiet true
// if true, stop the gradle build if errors are found
abortOnError false
// if true, only report errors
ignoreWarnings true
}
}
You can run lint for a specific variant (see below), e.g. ./gradlew lintRelease, or for all variants (./gradlew lint),
in which case it produces a report which describes which specific variants a given issue applies to.
GoalKicker.com – Android™ Notes for Professionals 1230
Check here for the DSL reference for all available options.
Section 255.3: Configuring lint checking in Java and XML
source files
You can disable Lint checking from your Java and XML source files.
Configuring lint checking in Java
To disable Lint checking specifically for a Java class or method in your Android project, add the @SuppressLint
annotation to that Java code.
Example:
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
To disable checking for all Lint issues:
@SuppressLint("all")
Configuring lint checking in XML
You can use the tools:ignore attribute to disable Lint checking for specific sections of your XML files.
For example:
tools:ignore="NewApi,StringFormatInvalid"
To suppress checking for all Lint issues in the XML element, use
tools:ignore="all"
Section 255.4: How to configure the lint.xml file
You can specify your Lint checking preferences in the lint.xml file. If you are creating this file manually, place it in
the root directory of your Android project. If you are configuring Lint preferences in Android Studio, the lint.xml file
is automatically created and added to your Android project for you.
Example:
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- list of issues to configure -->
</lint>
By setting the severity attribute value in the tag, you can disable Lint checking for an issue or change the severity
level for an issue.
The following example shows the contents of a lint.xml file.
<?xml version="1.0" encoding="UTF-8"?>
<lint>
GoalKicker.com – Android™ Notes for Professionals 1231
<!-- Disable the given check in this project -->
<issue id="IconMissingDensityFolder" severity="ignore" />
<!-- Ignore the ObsoleteLayoutParam issue in the specified files -->
<issue id="ObsoleteLayoutParam">
<ignore path="res/layout/activation.xml" />
<ignore path="res/layout-xlarge/activation.xml" />
</issue>
<!-- Ignore the UselessLeaf issue in the specified file -->
<issue id="UselessLeaf">
<ignore path="res/layout/main.xml" />
</issue>
<!-- Change the severity of hardcoded strings to "error" -->
<issue id="HardcodedText" severity="error" />
</lint>
Section 255.5: Mark Suppress Warnings
It's good practice to mark some warnings in your code. For example, some deprecated methods is need for your
testing, or old support version. But Lint checking will mark that code with warnings. For avoiding this problem, you
need use annotation @SuppressWarnings.
For example, add ignoring to warnings to deprecated methods. You need to put warnings description in annotation
also:
@SuppressWarnings("deprecated");
public void setAnotherColor (int newColor) {
getApplicationContext().getResources().getColor(newColor)
}
Using this annotation you can ignore all warnings, including Lint, Android, and other. Using Suppress Warnings,
helps to understand code correctly!
Section 255.6: Importing resources without "Deprecated"
error
Using the Android API 23 or higher, very often such situation can be seen:
This situation is caused by the structural change of the Android API regarding getting the resources. Now the
function:
public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException
should be used. But the android.support.v4 library has another solution.
Add the following dependency to the build.gradle file:
com.android.support:support-v4:24.0.0
Then all methods from support library are available:
GoalKicker.com – Android™ Notes for Professionals 1232
ContextCompat.getColor(context, R.color.colorPrimaryDark);
ContextCompat.getDrawable(context, R.drawable.btn_check);
ContextCompat.getColorStateList(context, R.color.colorPrimary);
DrawableCompat.setTint(drawable);
ContextCompat.getColor(context,R.color.colorPrimaryDark));
Moreover more methods from support library can be used:
ViewCompat.setElevation(textView, 1F);
ViewCompat.animate(textView);
TextViewCompat.setTextAppearance(textView, R.style.AppThemeTextStyle);
...
GoalKicker.com – Android™ Notes for Professionals 1233
Chapter 256: Performance Optimization
Your Apps performance is a crucial element of the user experience. Try to avoid bad performing patterns like doing
work on the UI thread and learn how to write fast and responsive apps.
Section 256.1: Save View lookups with the ViewHolder pattern
Especially in a ListView, you can run into performance problems by doing too many findViewById() calls during
scrolling. By using the ViewHolder pattern, you can save these lookups and improve your ListView performance.
If your list item contains a single TextView, create a ViewHolder class to store the instance:
static class ViewHolder {
TextView myTextView;
}
While creating your list item, attach a ViewHolder object to the list item:
public View getView(int position, View convertView, ViewGroup parent) {
Item i = getItem(position);
if(convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.list_item, parent, false);
// Create a new ViewHolder and save the TextView instance
ViewHolder holder = new ViewHolder();
holder.myTextView = (TextView)convertView.findViewById(R.id.my_text_view);
convertView.setTag(holder);
}
// Retrieve the ViewHolder and use the TextView
ViewHolder holder = (ViewHolder)convertView.getTag();
holder.myTextView.setText(i.getText());
return convertView;
}
Using this pattern, findViewById() will only be called when a new View is being created and the ListView can
recycle your views much more efficiently.
GoalKicker.com – Android™ Notes for Professionals 1234
Chapter 257: Android Kernel Optimization
Section 257.1: Low RAM Configuration
Android now supports devices with 512MB of RAM. This documentation is intended to help OEMs optimize and
configure Android 4.4 for low-memory devices. Several of these optimizations are generic enough that they can be
applied to previous releases as well.
Enable Low Ram Device flag
We are introducing a new API called ActivityManager.isLowRamDevice() for applications to determine if they should
turn off specific memory-intensive features that work poorly on low-memory devices.
For 512MB devices, this API is expected to return: "true" It can be enabled by the following system property in the
device makefile.
PRODUCT_PROPERTY_OVERRIDES += ro.config.low_ram=true
Disable JIT
System-wide JIT memory usage is dependent on the number of applications running and the code footprint of
those applications. The JIT establishes a maximum translated code cache size and touches the pages within it as
needed. JIT costs somewhere between 3M and 6M across a typical running system.
The large apps tend to max out the code cache fairly quickly (which by default has been 1M). On average, JIT cache
usage runs somewhere between 100K and 200K bytes per app. Reducing the max size of the cache can help
somewhat with memory usage, but if set too low will send the JIT into a thrashing mode. For the really low-memory
devices, we recommend the JIT be disabled entirely.
This can be achieved by adding the following line to the product makefile:
PRODUCT_PROPERTY_OVERRIDES += dalvik.vm.jit.codecachesize=0
Section 257.2: How to add a CPU Governor
The CPU governor itself is just 1 C file, which is located in kernel_source/drivers/cpufreq/, for example:
cpufreq_smartass2.c. You are responsible yourself for find the governor (look in an existing kernel repo for your
device) But in order to successfully call and compile this file into your kernel you will have to make the following
changes:
1. Copy your governor file (cpufreq_govname.c) and browse to kernel_source/drivers/cpufreq, now paste it.
2. and open Kconfig (this is the interface of the config menu layout) when adding a kernel, you want it to show
up in your config. You can do that by adding the choice of governor.
config CPU_FREQ_GOV_GOVNAMEHERE
tristate "'gov_name_lowercase' cpufreq governor"
depends on CPU_FREQ
help
governor' - a custom governor!
for example, for smartassV2.
config CPU_FREQ_GOV_SMARTASS2
tristate "'smartassV2' cpufreq governor"
GoalKicker.com – Android™ Notes for Professionals 1235
depends on CPU_FREQ
help
'smartassV2' - a "smart" optimized governor!
next to adding the choice, you also must declare the possibility that the governor gets chosen as default governor.
config CPU_FREQ_DEFAULT_GOV_GOVNAMEHERE
bool "gov_name_lowercase"
select CPU_FREQ_GOV_GOVNAMEHERE
help
Use the CPUFreq governor 'govname' as default.
for example, for smartassV2.
config CPU_FREQ_DEFAULT_GOV_SMARTASS2
bool "smartass2"
select CPU_FREQ_GOV_SMARTASS2
help
Use the CPUFreq governor 'smartassV2' as default.
– can’t find the right place to put it? Just search for “CPU_FREQ_GOV_CONSERVATIVE”, and place the code beneath,
same thing counts for “CPU_FREQ_DEFAULT_GOV_CONSERVATIVE”
Now that Kconfig is finished you can save and close the file.
3. While still in the /drivers/cpufreq folder, open Makefile. In Makefile, add the line corresponding to your
CPU Governor. for example:
obj-$(CONFIG_CPU_FREQ_GOV_SMARTASS2) += cpufreq_smartass2.o
Be ware that you do not call the native C file, but the O file! which is the compiled C file. Save the file.
4. Move to: kernel_source/includes/linux. Now open cpufreq.h Scroll down until you see something like:
#elif defined(CONFIG_CPU_FREQ_DEFAULT_GOV_ONDEMAND)
extern struct cpufreq_governor cpufreq_gov_ondemand;
#define CPUFREQ_DEFAULT_GOVERNOR (&amp;cpufreq_gov_ondemand)
(other cpu governors are also listed there)
Now add your entry with the selected CPU Governor, example:
#elif defined(CONFIG_CPU_FREQ_DEFAULT_GOV_SMARTASS2)
extern struct cpufreq_governor cpufreq_gov_smartass2;
#define CPUFREQ_DEFAULT_GOVERNOR (&amp;cpufreq_gov_smartass2)
Save the file and close it.
The initial CPU Governor setup is now complete. when you’ve done all steps successfully, you should be able to
choose your governor from the menu (menuconfig, xconfig, gconfig, nconfig). Once checked in the menu it will be
included to the kernel.
Commit that is nearly the same as above instructions: Add smartassV2 and lulzactive governor commit
GoalKicker.com – Android™ Notes for Professionals 1236
Section 257.3: I/O Schedulers
You can enhance your kernel by adding new I/O schedulers if needed. Globally, governors and schedulers are the
same; they both provide a way how the system should work. However, for the schedulers it is all about the
input/output datastream except for the CPU settings. I/O schedulers decide how an upcoming I/O activity will be
scheduled. The standard schedulers such as noop or cfq are performing very reasonably.
I/O schedulers can be found in kernel_source/block.
1. Copy your I/O scheduler file (for example, sio-iosched.c) and browse to kernel_source/block. Paste the
scheduler file there.
2. Now open Kconfig.iosched and add your choice to the Kconfig, for example for SIO:
config IOSCHED_SIO
tristate "Simple I/O scheduler"
default y
---help---
The Simple I/O scheduler is an extremely simple scheduler,
based on noop and deadline, that relies on deadlines to
ensure fairness. The algorithm does not do any sorting but
basic merging, trying to keep a minimum overhead. It is aimed
mainly for aleatory access devices (eg: flash devices).
3. Then set the default choice option as follows:
default "sio" if DEFAULT_SIO
Save the file.
4. Open the Makefile in kernel_source/block/ and simply add the following line for SIO:
obj-$(CONFIG_IOSCHED_SIO) += sio-iosched.o
Save the file and you are done! The I/O schedulers should now pop up at the menu config.
Similar commit on GitHub: added Simple I/O scheduler.
GoalKicker.com – Android™ Notes for Professionals 1237
Chapter 258: Memory Leaks
Section 258.1: Avoid leaking Activities with AsyncTask
A word of caution: AsyncTask has many gotcha's apart from the memory leak described here. So be
careful with this API, or avoid it altogether if you don't fully understand the implications. There are many
alternatives (Thread, EventBus, RxAndroid, etc).
One common mistake with AsyncTask is to capture a strong reference to the host Activity (or Fragment):
class MyActivity extends Activity {
private AsyncTask<Void, Void, Void> myTask = new AsyncTask<Void, Void, Void>() {
// Don't do this! Inner classes implicitly keep a pointer to their
// parent, which in this case is the Activity!
}
}
This is a problem because AsyncTask can easily outlive the parent Activity, for example if a configuration change
happens while the task is running.
The right way to do this is to make your task a static class, which does not capture the parent, and holding a weak
reference to the host Activity:
class MyActivity extends Activity {
static class MyTask extends AsyncTask<Void, Void, Void> {
// Weak references will still allow the Activity to be garbage-collected
private final WeakReference<MyActivity> weakActivity;
MyTask(MyActivity myActivity) {
this.weakActivity = new WeakReference<>(myActivity);
}
@Override
public Void doInBackground(Void... params) {
// do async stuff here
}
@Override
public void onPostExecute(Void result) {
// Re-acquire a strong reference to the activity, and verify
// that it still exists and is active.
MyActivity activity = weakActivity.get();
if (activity == null
|| activity.isFinishing()
|| activity.isDestroyed()) {
// activity is no longer valid, don't do anything!
return;
}
// The activity is still valid, do main-thread stuff here
}
}
}
GoalKicker.com – Android™ Notes for Professionals 1238
Section 258.2: Common memory leaks and how to fix them
1. Fix your contexts:
Try using the appropriate context: For example since a Toast can be seen in many activities instead of in just one,
use getApplicationContext() for toasts, and since services can keep running even though an activity has ended
start a service with:
Intent myService = new Intent(getApplicationContext(), MyService.class);
Use this table as a quick guide for what context is appropriate:
Original article on context here.
2. Static reference to Context
A serious memory leak mistake is keeping a static reference to View. Every View has an inner reference to the
Context. Which means an old Activity with its whole view hierarchy will not be garbage collected until the app is
terminated. You will have your app twice in memory when rotating the screen.
Make sure there is absolutely no static reference to View, Context or any of their descendants.
3. Check that you're actually finishing your services.
For example, I have an intentService that uses the Google location service API. And I forgot to call
googleApiClient.disconnect();:
//Disconnect from API onDestroy()
if (googleApiClient.isConnected()) {
LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient,
GoogleLocationService.this);
googleApiClient.disconnect();
}
4. Check image and bitmaps usage:
If you are using Square's library Picasso I found I was leaking memory by not using the .fit(), that drastically
reduced my memory footprint from 50MB in average to less than 19MB:
Picasso.with(ActivityExample.this) //Activity context
.load(object.getImageUrl())
.fit() //This avoided the OutOfMemoryError
GoalKicker.com – Android™ Notes for Professionals 1239
.centerCrop() //makes image to not stretch
.into(imageView);
5. If you are using broadcast receivers unregister them.
6. If you are using java.util.Observer (Observer pattern):
Make sure to use deleteObserver(observer);
Section 258.3: Detect memory leaks with the LeakCanary
library
LeakCanary is an Open Source Java library to detect memory leaks in your debug builds.
Just add the dependencies in the build.gradle:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
}
Then in your Application class:
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}
Now LeakCanary will automatically show a notification when an activity memory leak is detected in your debug
build.
NOTE: Release code will contain no reference to LeakCanary other than the two empty classes that exist in the
leakcanary-android-no-op dependency.
Section 258.4: Anonymous callback in activities
Every Time you create an anonymous class, it retains an implicit reference to its parent class. So when you write:
public class LeakyActivity extends Activity
{
...
foo.registerCallback(new BarCallback()
{
@Override
public void onBar()
GoalKicker.com – Android™ Notes for Professionals 1240
{
// do something
}
});
}
You are in fact sending a reference to your LeakyActivity instance to foo. When the user navigates away from your
LeakyActivity, this reference can prevent the LeakyActivity instance from being garbage collected. This is a serious
leak as activities hold a reference to their entire view hierarchy and are therefore rather large objects in memory.
How to avoid this leak:
You can of course avoid using anonymous callbacks in activities entirely. You can also unregister all of your
callbacks with respect to the activity lifecycle. like so:
public class NonLeakyActivity extends Activity
{
private final BarCallback mBarCallback = new BarCallback()
{
@Override
public void onBar()
{
// do something
}
});
@Override
protected void onResume()
{
super.onResume();
foo.registerCallback(mBarCallback);
}
@Override
protected void onPause()
{
super.onPause();
foo.unregisterCallback(mBarCallback);
}
}
Section 258.5: Activity Context in static classes
Often you will want to wrap some of Android's classes in easier to use utility classes. Those utility classes often
require a context to access the android OS or your apps' resources. A common example of this is a wrapper for the
SharedPreferences class. In order to access Androids shared preferences one must write:
context.getSharedPreferences(prefsName, mode);
And so one may be tempted to create the following class:
public class LeakySharedPrefsWrapper
{
private static Context sContext;
public static void init(Context context)
{
sContext = context;
}
GoalKicker.com – Android™ Notes for Professionals 1241
public int getInt(String name,int defValue)
{
return sContext.getSharedPreferences("a name", Context.MODE_PRIVATE).getInt(name,defValue);
}
}
now, if you call init() with your activity context, the LeakySharedPrefsWrapper will retain a reference to your
activity, preventing it from being garbage collected.
How to avoid:
When calling static helper functions, you can send in the application context using
context.getApplicationContext();
When creating static helper functions, you can extract the application context from the context you are given
(Calling getApplicationContext() on the application context returns the application context). So the fix to our
wrapper is simple:
public static void init(Context context)
{
sContext = context.getApplicationContext();
}
If the application context is not appropriate for your use case, you can include a Context parameter in each utility
function, you should avoid keeping references to these context parameters. In this case the solution would look like
so:
public int getInt(Context context,String name,int defValue)
{
// do not keep a reference of context to avoid potential leaks.
return context.getSharedPreferences("a name", Context.MODE_PRIVATE).getInt(name,defValue);
}
Section 258.6: Avoid leaking Activities with Listeners
If you implement or create a listener in an Activity, always pay attention to the lifecycle of the object that has the
listener registered.
Consider an application where we have several different activities/fragments interested in when a user is logged in
or out. One way of doing this would be to have a singleton instance of a UserController that can be subscribed to
in order to get notified when the state of the user changes:
public class UserController {
private static UserController instance;
private List<StateListener> listeners;
public static synchronized UserController getInstance() {
if (instance == null) {
instance = new UserController();
}
return instance;
}
private UserController() {
// Init
}
GoalKicker.com – Android™ Notes for Professionals 1242
public void registerUserStateChangeListener(StateListener listener) {
listeners.add(listener);
}
public void logout() {
for (StateListener listener : listeners) {
listener.userLoggedOut();
}
}
public void login() {
for (StateListener listener : listeners) {
listener.userLoggedIn();
}
}
public interface StateListener {
void userLoggedIn();
void userLoggedOut();
}
}
Then there are two activities, SignInActivity:
public class SignInActivity extends Activity implements UserController.StateListener{
UserController userController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.userController = UserController.getInstance();
this.userController.registerUserStateChangeListener(this);
}
@Override
public void userLoggedIn() {
startMainActivity();
}
@Override
public void userLoggedOut() {
showLoginForm();
}
...
public void onLoginClicked(View v) {
userController.login();
}
}
And MainActivity:
public class MainActivity extends Activity implements UserController.StateListener{
UserController userController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GoalKicker.com – Android™ Notes for Professionals 1243
this.userController = UserController.getInstance();
this.userController.registerUserStateChangeListener(this);
}
@Override
public void userLoggedIn() {
showUserAccount();
}
@Override
public void userLoggedOut() {
finish();
}
...
public void onLogoutClicked(View v) {
userController.logout();
}
}
What happens with this example is that every time the user logs in and then logs out again, a MainActivity
instance is leaked. The leak occurs because there is a reference to the activity in UserController#listeners.
Please note: Even if we use an anonymous inner class as a listener, the activity would still leak:
...
this.userController.registerUserStateChangeListener(new UserController.StateListener() {
@Override
public void userLoggedIn() {
showUserAccount();
}
@Override
public void userLoggedOut() {
finish();
}
});
...
The activity would still leak, because the anonymous inner class has an implicit reference to the outer class (in this
case the activity). This is why it is possible to call instance methods in the outer class from the inner class. In fact,
the only type of inner classes that do not have a reference to the outer class are static inner classes.
In short, all instances of non-static inner classes hold an implicit reference to the instance of the outer class that
created them.
There are two main approaches to solving this, either by adding a method to remove a listener from
UserController#listeners or using a WeakReference to hold the reference of the listeners.
Alternative 1: Removing listeners
Let us start by creating a new method removeUserStateChangeListener(StateListener listener):
public class UserController {
...
public void registerUserStateChangeListener(StateListener listener) {
GoalKicker.com – Android™ Notes for Professionals 1244
listeners.add(listener);
}
public void removeUserStateChangeListener(StateListener listener) {
listeners.remove(listener);
}
...
}
Then let us call this method in the activity's onDestroy method:
public class MainActivity extends Activity implements UserController.StateListener{
...
@Override
protected void onDestroy() {
super.onDestroy();
userController.removeUserStateChangeListener(this);
}
}
With this modification the instances of MainActivity are no longer leaked when the user logs in and out. However,
if the documentation isn't clear, chances are that the next developer that starts using UserController might miss
that it is required to unregister the listener when the activity is destroyed, which leads us to the second method of
avoiding these types of leaks.
Alternative 2: Using weak references
First off, let us start by explaining what a weak reference is. A weak reference, as the name suggests, holds a weak
reference to an object. Compared to a normal instance field, which is a strong reference, a weak references does
not stop the garbage collector, GC, from removing the objects. In the example above this would allow MainActivity
to be garbage-collected after it has been destroyed if the UserController used WeakReference to the reference the
listeners.
In short, a weak reference is telling the GC that if no one else has a strong reference to this object, go ahead and
remove it.
Let us modify the UserController to use a list of WeakReference to keep track of it's listeners:
public class UserController {
...
private List<WeakReference<StateListener>> listeners;
...
public void registerUserStateChangeListener(StateListener listener) {
listeners.add(new WeakReference<>(listener));
}
public void removeUserStateChangeListener(StateListener listenerToRemove) {
WeakReference referencesToRemove = null;
for (WeakReference<StateListener> listenerRef : listeners) {
StateListener listener = listenerRef.get();
if (listener != null && listener == listenerToRemove) {
referencesToRemove = listenerRef;
break;
}
GoalKicker.com – Android™ Notes for Professionals 1245
}
listeners.remove(referencesToRemove);
}
public void logout() {
List referencesToRemove = new LinkedList();
for (WeakReference<StateListener> listenerRef : listeners) {
StateListener listener = listenerRef.get();
if (listener != null) {
listener.userLoggedOut();
} else {
referencesToRemove.add(listenerRef);
}
}
}
public void login() {
List referencesToRemove = new LinkedList();
for (WeakReference<StateListener> listenerRef : listeners) {
StateListener listener = listenerRef.get();
if (listener != null) {
listener.userLoggedIn();
} else {
referencesToRemove.add(listenerRef);
}
}
}
...
}
With this modification it doesn't matter whether or not the listeners are removed, since UserController holds no
strong references to any of the listeners. However, writing this boilerplate code every time is cumbersome.
Therefore, let us create a generic class called WeakCollection:
public class WeakCollection<T> {
private LinkedList<WeakReference<T>> list;
public WeakCollection() {
this.list = new LinkedList<>();
}
public void put(T item){
//Make sure that we don't re add an item if we already have the reference.
List<T> currentList = get();
for(T oldItem : currentList){
if(item == oldItem){
return;
}
}
list.add(new WeakReference<T>(item));
}
public List<T> get() {
List<T> ret = new ArrayList<>(list.size());
List<WeakReference<T>> itemsToRemove = new LinkedList<>();
for (WeakReference<T> ref : list) {
T item = ref.get();
if (item == null) {
itemsToRemove.add(ref);
} else {
ret.add(item);
}
GoalKicker.com – Android™ Notes for Professionals 1246
}
for (WeakReference ref : itemsToRemove) {
this.list.remove(ref);
}
return ret;
}
public void remove(T listener) {
WeakReference<T> refToRemove = null;
for (WeakReference<T> ref : list) {
T item = ref.get();
if (item == listener) {
refToRemove = ref;
}
}
if(refToRemove != null){
list.remove(refToRemove);
}
}
}
Now let us re-write UserController to use WeakCollection<T> instead:
public class UserController {
...
private WeakCollection<StateListener> listenerRefs;
...
public void registerUserStateChangeListener(StateListener listener) {
listenerRefs.put(listener);
}
public void removeUserStateChangeListener(StateListener listenerToRemove) {
listenerRefs.remove(listenerToRemove);
}
public void logout() {
for (StateListener listener : listenerRefs.get()) {
listener.userLoggedOut();
}
}
public void login() {
for (StateListener listener : listenerRefs.get()) {
listener.userLoggedIn();
}
}
...
}
As shown in the code example above, the WeakCollection<T> removes all of the boilerplate code needed to use
WeakReference instead of a normal list. To top it all off: If a call to
UserController#removeUserStateChangeListener(StateListener) is missed, the listener, and all the objects it is
referencing, will not leak.
Section 258.7: Avoid memory leaks with Anonymous Class,
GoalKicker.com – Android™ Notes for Professionals 1247
Handler, Timer Task, Thread
In android, every developer uses Anonymous Class (Runnable) at least once in a project. Any Anonymous Class has a
reference to its parent (activity). If we perform a long-running task, the parent activity will not be destroyed until the
task is ended.
Example uses handler and Anonymous Runnable class. The memory will be leak when we quit the activity before
the Runnable is finished.
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// do abc long 5s or so
}
}, 10000); // run "do abc" after 10s. It same as timer, thread...
How do we solve it?
1. Don't do any long operating with Anonymous Class or we need a Static class for it and pass WeakReference
into it (such as activity, view...). Thread is the same with Anonymous Class.
2. Cancel the Handler, Timer when activity is destroyed.
GoalKicker.com – Android™ Notes for Professionals 1248
Chapter 259: Enhancing Android
Performance Using Icon Fonts
Section 259.1: How to integrate Icon fonts
In order to use icon fonts, just follow the steps below:
Add the font file to your project
You may create your font icon file from online websites such as icomoon, where you can upload SVG files of
the required icons and then download the created icon font. Then, place the .ttf font file into a folder named
fonts (name it as you wish) in the assets folder:
Create a Helper Class
Now, create the following helper class, so that you can avoid repeating the initialisation code for the font:
public class FontManager {
public static final String ROOT = "fonts/";
FONT_AWESOME = ROOT + "myfont.ttf";
public static Typeface getTypeface(Context context) {
return Typeface.createFromAsset(context.getAssets(), FONT_AWESOME);
}
}
You may use the Typeface class in order to pick the font from the assets. This way you can set the typeface
to various views, for example, to a button:
Button button=(Button) findViewById(R.id.button);
Typeface iconFont=FontManager.getTypeface(getApplicationContext());
button.setTypeface(iconFont);
Now, the button typeface has been changed to the newly created icon font.
GoalKicker.com – Android™ Notes for Professionals 1249
Pick up the icons you want
Open the styles.css file attached to the icon font. There you will find the styles with Unicode characters of your
icons:
.icon-arrow-circle-down:before {
content: “\e001”;
}
.icon-arrow-circle-left:before {
content: “\e002”;
}
.icon-arrow-circle-o-down:before {
content: “\e003”;
}
.icon-arrow-circle-o-left:before {
content: “\e004”;
}
This resource file will serve as a dictionary, which maps the Unicode character associated with a specific icon
to a human-readable name. Now, create the string resources as follows:
<resources>
<! — Icon Fonts -->
<string name=”icon_arrow_circle_down”>&#xe001; </string>
<string name=”icon_arrow_circle_left”>&#xe002; </string>
<string name=”icon_arrow_circle-o_down”>&#xe003; </string>
<string name=”icon_arrow_circle_o_left”>&#xe004; </string>
</resources>
Use the icons in your code
Now, you may use your font in various views, for example, as follows:
button.setText(getString(R.string.icon_arrow_circle_left))
You may also create button text views using icon fonts:
GoalKicker.com – Android™ Notes for Professionals 1250
Section 259.2: TabLayout with icon fonts
public class TabAdapter extends FragmentPagerAdapter {
CustomTypefaceSpan fonte;
List<Fragment> fragments = new ArrayList<>(4);
private String[] icons = {"\ue001","\uE002","\uE003","\uE004"};
public TabAdapter(FragmentManager fm, CustomTypefaceSpan fonte) {
super(fm);
this.fonte = fonte
for (int i = 0; i < 4; i++){
fragments.add(MyFragment.newInstance());
}
}
public List<Fragment> getFragments() {
return fragments;
}
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
GoalKicker.com – Android™ Notes for Professionals 1251
@Override
public CharSequence getPageTitle(int position) {
SpannableStringBuilder ss = new SpannableStringBuilder(icons[position]);
ss.setSpan(fonte,0,ss.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
ss.setSpan(new RelativeSizeSpan(1.5f),0,ss.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE );
return ss;
}
@Override
public int getCount() {
return 4;
}
}
In this example, myfont.ttf is in Assets folder. Creating Assets folder
In your activity class
//..
TabLayout tabs;
ViewPager tabs_pager;
public CustomTypefaceSpan fonte;
//..
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
fm = getSupportFragmentManager();
fonte = new CustomTypefaceSpan("icomoon",Typeface.createFromAsset(getAssets(),"myfont.ttf"));
this.tabs = ((TabLayout) hasViews.findViewById(R.id.tabs));
this.tabs_pager = ((ViewPager) hasViews.findViewById(R.id.tabs_pager));
//...
}
@Override
protected void onStart() {
super.onStart();
//..
tabs_pager.setAdapter(new TabAdapter(fm,fonte));
tabs.setupWithViewPager(tabs_pager);
//..
GoalKicker.com – Android™ Notes for Professionals 1252
Chapter 260: Bitmap Cache
Parameter Details
key key to store bitmap in memory cache
bitmap bitmap value which will cache into memory
Memory efficient bitmap caching: This is particularly important if your application uses animations as they will be
stopped during GC cleanup and make your application appears sluggish to the user. A cache allows reusing objects
which are expensive to create. If you load on object into memory, you can think of this as a cache for the
object.Working with bitmap in android is tricky.It is more important to cache the bimap if you are going to use it
repeatedly.
Section 260.1: Bitmap Cache Using LRU Cache
LRU Cache
The following example code demonstrates a possible implementation of the LruCache class for caching images.
private LruCache<String, Bitmap> mMemoryCache;
Here string value is key for bitmap value.
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
For add bitmap to the memory cache
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
For get bitmap from memory cache
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
For loading bitmap into imageview just use getBitmapFromMemCache("Pass key").
GoalKicker.com – Android™ Notes for Professionals 1253
Chapter 261: Loading Bitmaps Eectively
This Topic Mainly Concentrate on Loading the Bitmaps Effectively in Android Devices.
When it comes to loading a bitmap, the question comes where it is loaded from. Here we are going to discuss about
how to load the Bitmap from Resource with in the Android Device. i.e. eg from Gallery.
We will go through this by example which are discussed below.
Section 261.1: Load the Image from Resource from Android
Device. Using Intents
Using Intents to Load the Image from Gallery.
1. Initially you need to have the permission
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
2. Use the Following Code to have the layout as designed follows.
GoalKicker.com – Android™ Notes for Professionals 1254
GoalKicker.com – Android™ Notes for Professionals 1255
GoalKicker.com – Android™ Notes for Professionals 1256
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="androidexamples.idevroids.loadimagefrmgallery.MainActivity">
<ImageView
android:id="@+id/imgView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@color/abc_search_url_text_normal"></ImageView>
<Button
android:id="@+id/buttonLoadPicture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:text="Load Picture"
android:layout_gravity="bottom|center"></Button>
</LinearLayout>
3. Use the Following code to Display the image with button Click.
Button Click will be
Button loadImg = (Button) this.findViewById(R.id.buttonLoadPicture);
loadImg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(i, RESULT_LOAD_IMAGE);
}
});
3. Once you clicked on the button , it will open the gallery with help of intent.
You need to select image and send it back to main activity. Here with help of onActivityResult we can do that.
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RESULT_LOAD_IMAGE && resultCode == RESULT_OK && null != data) {
Uri selectedImage = data.getData();
String[] filePathColumn = { MediaStore.Images.Media.DATA };
Cursor cursor = getContentResolver().query(selectedImage,
filePathColumn, null, null, null);
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
String picturePath = cursor.getString(columnIndex);
cursor.close();
GoalKicker.com – Android™ Notes for Professionals 1257
ImageView imageView = (ImageView) findViewById(R.id.imgView);
imageView.setImageBitmap(BitmapFactory.decodeFile(picturePath));
}
}
GoalKicker.com – Android™ Notes for Professionals 1258
Chapter 262: Exceptions
Section 262.1: ActivityNotFoundException
This is a very common Exception. It causes your application to stop during the start or execution of your app. In the
LogCat you see the message:
android.content.ActivityNotFoundException : Unable to find explicit activity class;
have you declared this activity in your AndroidManifest.xml?
In this case, check if you have declared your activity in the AndroidManifest.xml file.
The simplest way to declare your Activity in AndroidManifest.xml is:
<activity android:name="com.yourdomain.YourStoppedActivity" />
Section 262.2: OutOfMemoryError
This is a runtime error that happens when you request a large amount of memory on the heap. This is common
when loading a Bitmap into an ImageView.
You have some options:
1. Use a large application heap
Add the "largeHeap" option to the application tag in your AndroidManifest.xml. This will make more memory
available to your app but will likely not fix the root issue.
<application largeHeap="true" ... >
2. Recycle your bitmaps
After loading a bitmap, be sure to recycle it and free up memory:
if (bitmap != null && !bitmap.isRecycled())
bitmap.recycle();
3. Load sampled bitmaps into memory
Avoid loading the entire bitmap into memory at once by sampling a reduced size, using BitmapOptions and
inSampleSize.
See Android documentation for example
Section 262.3: Registering own Handler for unexpected
exceptions
This is how you can react to exceptions which have not been catched, similar to the system's standard "Application
XYZ has crashed"
import android.app.Application;
import android.util.Log;
import java.io.File;
GoalKicker.com – Android™ Notes for Professionals 1259
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* Application class writing unexpected exceptions to a crash file before crashing.
*/
public class MyApplication extends Application {
private static final String TAG = "ExceptionHandler";
@Override
public void onCreate() {
super.onCreate();
// Setup handler for uncaught exceptions.
final Thread.UncaughtExceptionHandler defaultHandler =
Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable e) {
try {
handleUncaughtException(e);
System.exit(1);
} catch (Throwable e2) {
Log.e(TAG, "Exception in custom exception handler", e2);
defaultHandler.uncaughtException(thread, e);
}
}
});
}
private void handleUncaughtException(Throwable e) throws IOException {
Log.e(TAG, "Uncaught exception logged to local file", e);
// Create a new unique file
final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US);
String timestamp;
File file = null;
while (file == null || file.exists()) {
timestamp = dateFormat.format(new Date());
file = new File(getFilesDir(), "crashLog_" + timestamp + ".txt");
}
Log.i(TAG, "Trying to create log file " + file.getPath());
file.createNewFile();
// Write the stacktrace to the file
FileWriter writer = null;
try {
writer = new FileWriter(file, true);
for (StackTraceElement element : e.getStackTrace()) {
writer.write(element.toString());
}
} finally {
if (writer != null) writer.close();
}
// You can (and probably should) also display a dialog to notify the user
}
}
GoalKicker.com – Android™ Notes for Professionals 1260
Then register this Application class in your AndroidManifest.xml:
<application android:name="de.ioxp.arkmobile.MyApplication" >
Section 262.4: UncaughtException
If you want to handle uncaught exceptions try to catch them all in onCreate method of you Application class:
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
try {
Thread
.setDefaultUncaughtExceptionHandler(
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable e) {
Log.e(TAG,
"Uncaught Exception thread: "+thread.getName()+"
"+e.getStackTrace());
handleUncaughtException (thread, e);
}
});
} catch (SecurityException e) {
Log.e(TAG,
"Could not set the Default Uncaught Exception Handler:"
+e.getStackTrace());
}
}
private void handleUncaughtException (Thread thread, Throwable e){
Log.e(TAG, "uncaughtException:");
e.printStackTrace();
}
}
Section 262.5: NetworkOnMainThreadException
From the documentation:
The exception that is thrown when an application attempts to perform a networking operation on its
main thread.
This is only thrown for applications targeting the Honeycomb SDK or higher. Applications targeting earlier
SDK versions are allowed to do networking on their main event loop threads, but it's heavily discouraged.
Here's an example of a code fragment that may cause that exception:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
GoalKicker.com – Android™ Notes for Professionals 1261
Uri.Builder builder = new Uri.Builder().scheme("http").authority("www.google.com");
HttpURLConnection urlConnection = null;
BufferedReader reader = null;
URL url;
try {
url = new URL(builder.build().toString());
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.connect();
} catch (IOException e) {
Log.e("TAG","Connection error", e);
} finally{
if (urlConnection != null) {
urlConnection.disconnect();
}
if (reader != null) {
try {
reader.close();
} catch (final IOException e) {
Log.e("TAG", "Error closing stream", e);
}
}
}
}
}
Above code will throw NetworkOnMainThreadException for applications targeting Honeycomb SDK (Android v3.0) or
higher as the application is trying to perform a network operation on the main thread.
To avoid this exception, your network operations must always run in a background task via an AsyncTask, Thread,
IntentService, etc.
private class MyAsyncTask extends AsyncTask<String, Integer, Void> {
@Override
protected Void doInBackground(String[] params) {
Uri.Builder builder = new Uri.Builder().scheme("http").authority("www.google.com");
HttpURLConnection urlConnection = null;
BufferedReader reader = null;
URL url;
try {
url = new URL(builder.build().toString());
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.connect();
} catch (IOException e) {
Log.e("TAG","Connection error", e);
} finally{
if (urlConnection != null) {
urlConnection.disconnect();
}
if (reader != null) {
try {
reader.close();
} catch (final IOException e) {
Log.e("TAG", "Error closing stream", e);
}
}
}
return null;
GoalKicker.com – Android™ Notes for Professionals 1262
}
}
Section 262.6: DexException
com.android.dex.DexException: Multiple dex files define Lcom/example/lib/Class;
This error occurs because the app, when packaging, finds two .dex files that define the same set of methods.
Usually this happens because the app has accidentally acquired 2 separate dependencies on the same library.
For instance, say you have a project, and you want to rely on two libraries: A and B, which each have their own
dependencies. If library B already has a dependency on library A, this error will be thrown if library A is added to the
project by itself. Compiling library B already gave the set of code from A, so when the compiler goes to bundle
library A, it finds library A's methods already packaged.
To resolve, make sure that none of your dependencies could accidentally be added twice in such a manner
GoalKicker.com – Android™ Notes for Professionals 1263
Chapter 263: Logging and using Logcat
Option Description
-b (buffer) Loads an alternate log buffer for viewing, such as events or radio. The main buffer is used by default.
See Viewing Alternative Log Buffers.
-c Clears (flushes) the entire log and exits.
-d Dumps the log to the screen and exits.
-f (filename) Writes log message output to (filename). The default is stdout.
-g Prints the size of the specified log buffer and exits.
-n (count) Sets the maximum number of rotated logs to (count). The default value is 4. Requires the -r option.
-r (kbytes) Rotates the log file every (kbytes) of output. The default value is 16. Requires the -f option.
-s Sets the default filter spec to silent.
-v (format) Sets the output format for log messages. The default is brief format.
Section 263.1: Filtering the logcat output
It is helpful to filter the logcat output because there are many messages which are not of interest. To filter the
output, open the "Android Monitor" and click on the drop down on the top-right and select Edit Filter Configuration
Now you can add custom filters to show messages which are of interest, as well as filter out well-known log lines
which can safely be ignored. To ignore a part of the output you may define a Regular Expression. Here is an
example of excluding matching tags:
^(?!(HideMe|AndThis))
This can be entered by following this example:
GoalKicker.com – Android™ Notes for Professionals 1264
The above is a regular expression which excludes inputs. If you wanted to add another tag to the blacklist, add it
after a pipe | character. For example, if you wanted to blacklist "GC", you would use a filter like this:
^(?!(HideMe|AndThis|GC))
For more documentation and examples visit Logging and using Logcat
Section 263.2: Logging
Any quality Android application will keep track of what it's doing through application logs. These logs allow easy
debugging help for the developer to diagnose what's going on with the application. Full Android Documentation can
be found here, but a summary follows:
Basic Logging
The Log class is the main source of writing developer logs, by specifying a tag and a message. The tag is what you
can use to filter log messages by to identify which lines come from your particular Activity. Simply call
Log.v(String tag, String msg);
And the Android system will write a message to the logcat:
07-28 12:00:00.759 24812-24839/my.packagename V/MyAnimator: Some log messages
└ time stamp | app.package┘ | └ any tag |
process & thread ids ┘ log level┘ └ the log message
TIP:
Notice the process id and the thread id. If they are the same - the log is coming from the main/UI thread!
Any tag can be used, but it is common to use the class name as a tag:
GoalKicker.com – Android™ Notes for Professionals 1265
public static final String tag = MyAnimator.class.getSimpleName();
Log Levels
The Android logger has 6 different levels, each of which serve a certain purpose:
ERROR: Log.e()
Used to indicate critical failure, this is the level printed at when throwing an Exception.
WARN: Log.w()
Used to indicate a warning, mainly for recoverable failures
INFO: Log.i()
Used to indicate higher-level information about the state of the application
DEBUG: Log.d()
Used to log information that would be useful to know when debugging the application, but would get
in the way when running the application
VERBOSE: Log.v()
Used to log information that reflects the small details about the state of the application
ASSERT: Log.wtf()
Used to log information about a condition that should never happen.
wtf stands for "What a Terrible Failure".
Motivation For Logging
The motivation for logging is to easily find errors, warnings, and other information by glancing at the chain of
events from the application. For instance, imagine an application that reads lines from a text file, but incorrectly
assumes that the file will never be empty. The log trace (of an app that doesn't log) would look something like this:
E/MyApplication: Process: com.example.myapplication, PID: 25788
com.example.SomeRandomException: Expected string, got 'null' instead
Followed by a bunch of stack traces that would eventually lead to the offending line, where stepping through with a
debugger would eventually lead to the problem
However, the log trace of an application with logging enabled could look something like this:
V/MyApplication: Looking for file myFile.txt on the SD card
D/MyApplication: Found file myFile.txt at path <path>
V/MyApplication: Opening file myFile.txt
D/MyApplication: Finished reading myFile.txt, found 0 lines
V/MyApplication: Closing file myFile.txt
...
E/MyApplication: Process: com.example.myapplication, PID: 25788
com.example.SomeRandomException: Expected string, got 'null' instead
A quick glance at the logs and it is obvious that the file was empty.
Things To Considering When Logging:
Although logging is a powerful tool that allows Android developers to gain a greater insight into the inner working
of their application, logging does have some drawbacks.
Log Readability:
It is common for Android Applications to have several logs running synchronously. As such, it is very important that
each log is easily readable and only contains relevant, necessary information.
Performance:
Logging does require a small amount of system resources. In general, this does not warrant concern, however, if
GoalKicker.com – Android™ Notes for Professionals 1266
overused, logging may have a negative impact on application performance.
Security:
Recently, several Android Applications have been added to the Google Play marketplace that allow the user to view
logs of all running applications. This unintended display of data may allow users to view confidential information.
As a rule of thumb, always remove logs that contain on non-public data before publishing your application to the
marketplace.
Conclusion:
Logging is an essential part of an Android application, because of the power it gives to developers. The ability to
create a useful log trace is one of the most challenging aspects of software development, but Android's Log class
helps to make it much easier.
For more documentation and examples visit Logging and using Logcat
Section 263.3: Using the Logcat
Logcat is a command-line tool that dumps a log of system messages, including stack traces when the device throws
an error and messages that you have written from your app with the Log class.
The Logcat output can be displayed within Android Studio's Android Monitor or with adb command line.
In Android Studio
Show by clicking the "Android Monitor" icon:
Or
by pressing Alt + 6 on Windows/Linux or CMD + 6 on Mac.
via command line:
Simple usage:
$ adb logcat
With timestamps:
$ adb logcat -v time
Filter on specific text:
$ adb logcat -v time | grep 'searchtext'
There are many options and filters available to command line logcat, documented here.
A simple but useful example is the following filter expression that displays all log messages with priority level
"error", on all tags:
$ adb logcat *:E
GoalKicker.com – Android™ Notes for Professionals 1267
Section 263.4: Log with link to source directly from Logcat
This is a nice trick to add a link to code, so it will be easy to jump to the code that issued the log.
With the following code, this call:
MyLogger.logWithLink("MyTag","param="+param);
Will result in:
07-26...012/com.myapp D/MyTag: MyFrag:onStart(param=3) (MyFrag.java:2366) // << logcat converts
this to a link to source!
This is the code (inside a class called MyLogger):
static StringBuilder sb0 = new StringBuilder(); // reusable string object
public static void logWithLink(String TAG, Object param) {
StackTraceElement stack = Thread.currentThread().getStackTrace()[3];
sb0.setLength(0);
String c = stack.getFileName().substring(0, stack.getFileName().length() - 5); // removes the
".java"
sb0.append(c).append(":");
sb0.append(stack.getMethodName()).append('(');
if (param != null) {
sb0.append(param);
}
sb0.append(") ");
sb0.append("
(").append(stack.getFileName()).append(':').append(stack.getLineNumber()).append(')');
Log.d(TAG, sb0.toString());
}
This is a basic example, it can be easily extended to issue a link to the caller (hint: the stack will be [4] instead of [3]),
and you can also add other relevant information.
Section 263.5: Clear logs
In order to clear (flush) the entire log:
adb logcat -c
Section 263.6: Android Studio usage
1. Hide/show printed information:
GoalKicker.com – Android™ Notes for Professionals 1268
2. Control verbosity of the logging:
3. Disable/enable opening log window when starting run/debug application
Section 263.7: Generating Logging code
Android Studio's Live templates can offer quite a few shortcuts for quick logging.
To use Live templates, all you need to do is to start typing the template name, and hit TAB or enter to insert the
statement.
Examples:
logi → turns into → android.util.Log.i(TAG, "$METHOD_NAME$: $content$");
$METHOD_NAME$ will automatically be replaced with your method name, and the cursor will wait for the
content to be filled.
loge → same, for error
etc. for the rest of the logging levels.
Full list of templates can be found in Android Studio's settings ( ALT + s and type "live"). And it is possible to
GoalKicker.com – Android™ Notes for Professionals 1269
add your custom templates as well.
If you find Android Studio's Live templates not enough for your needs, you can consider Android Postfix Plugin
This is a very useful library which helps you to avoid writing the logging line manually.
The syntax is absolutely simple:
.log - Logging. If there is constant variable "TAG", it use "TAG" . Else it use class name.
GoalKicker.com – Android™ Notes for Professionals 1270
Chapter 264: ADB (Android Debug Bridge)
ADB (Android Debug Bridge) is a command line tool that used to communicate with an emulator instance or
connected Android device.
Overview of ADB
A large portion of this topic was split out to adb shell
Section 264.1: Connect ADB to a device via WiFi
The standard ADB configuration involves a USB connection to a physical device.
If you prefer, you can switch over to TCP/IP mode, and connect ADB via WiFi instead.
Not rooted device
1. Get on the same network:
Make sure your device and your computer are on the same network.
2. Connect the device to the host computer with a USB cable.
3. Connect adb to device over network:
While your device is connected to adb via USB, do the following command to listen for a TCP/IP connection on
a port (default 5555):
Type adb tcpip <port> (switch to TCP/IP mode).
Disconnect the USB cable from the target device.
Type adb connect <ip address>:<port> (port is optional; default 5555).
For example:
adb tcpip 5555
adb connect 192.168.0.101:5555
If you don't know your device's IP you can:
check the IP in the WiFi settings of your device.
use ADB to discover IP (via USB):
1. Connect the device to the computer via USB
2. In a command line, type adb shell ifconfig and copy your device's IP address
To revert back to debugging via USB use the following command:
adb usb
You can also connect ADB via WiFi by installing a plugin to Android Studio. In order to do so, go to Settings >
Plugins and Browse repositories, search for ADB WiFi, install it, and reopen Android Studio. You will see a new
icon in your toolbar as shown in the following image. Connect the device to the host computer via USB and
click on this AndroidWiFiADB icon. It will display a message whether your device is connected or not. Once it
gets connected you can unplug your USB.
GoalKicker.com – Android™ Notes for Professionals 1271
Rooted device
Note: Some devices which are rooted can use the ADB WiFi App from the Play Store to enable this in a simple way.
Also, for certain devices (especially those with CyanogenMod ROMs) this option is present in the Developer Options
among the Settings. Enabling it will give you the IP address and port number required to connect to adb by simply
executing adb connect <ip address>:<port>.
When you have a rooted device but don't have access to a USB cable
The process is explained in detail in the following answer:
http://stackoverflow.com/questions/2604727/how-can-i-connect-to-android-with-adb-over-tcp/3623727#3623727
The most important commands are shown below.
Open a terminal in the device and type the following:
su
setprop service.adb.tcp.port <a tcp port number>
stop adbd
start adbd
For example:
setprop service.adb.tcp.port 5555
And on your computer:
adb connect <ip address>:<a tcp port number>
For example:
adb connect 192.168.1.2:5555
To turn it off:
setprop service.adb.tcp.port -1
stop adbd
start adbd
Avoid timeout
By default adb will timeout after 5000 ms. This can happen in some cases such as slow WiFi or large APK.
A simple change in the Gradle configuration can do the trick:
android {
adbOptions {
timeOutInMs 10 * 1000
}
}
GoalKicker.com – Android™ Notes for Professionals 1272
Section 264.2: Direct ADB command to specific device in a
multi-device setting
1. Target a device by serial number
Use the -s option followed by a device name to select on which device the adb command should run. The -s
options should be first in line, before the command.
adb -s <device> <command>
Example:
adb devices
List of devices attached
emulator-5554 device
02157df2d1faeb33 device
adb -s emulator-5554 shell
Example#2:
adb devices -l
List of devices attached
06157df65c6b2633 device usb:1-3 product:zerofltexx model:SM_G920F device:zeroflte
LC62TB413962 device usb:1-5 product:a50mgp_dug_htc_emea model:HTC_Desire_820G_dual_sim
device:htc_a50mgp_dug
adb -s usb:1-3 shell
2. Target a device, when only one device type is connected
You can target the only running emulator with -e
adb -e <command>
Or you can target the only connected USB device with -d
adb -d <command>
Section 264.3: Taking a screenshot and video (for kitkat only)
from a device display
Screen shot: Option 1 (pure adb)
The shell adb command allows us to execute commands using a device's built-in shell. The screencap shell
command captures the content currently visible on a device and saves it into a given image file, e.g.
/sdcard/screen.png:
adb shell screencap /sdcard/screen.png
You can then use the pull command to download the file from the device into the current directory on you
computer:
GoalKicker.com – Android™ Notes for Professionals 1273
adb pull /sdcard/screen.png
Screen shot:Option 2 (faster)
Execute the following one-liner:
(Marshmallow and earlier):
adb shell screencap -p | perl -pe 's/\x0D\x0A/\x0A/g' > screen.png
(Nougat and later):
adb shell screencap -p > screen.png
The -p flag redirects the output of the screencap command to stdout. The Perl expression this is piped into cleans
up some end-of-line issues on Marshmallow and earlier. The stream is then written to a file named screen.png
within the current directory. See this article and this article for more information.
Video
this only work in KitKat and via ADB only. This not Working below Kitkat To start recording your device’s screen, run
the following command:
adb shell screenrecord /sdcard/example.mp4, This command will start recording your device’s screen using the
default settings and save the resulting video to a file at /sdcard/example.mp4 file on your device.
When you’re done recording, press Ctrl+C (z in Linux) in the Command Prompt window to stop the screen
recording. You can then find the screen recording file at the location you specified. Note that the screen recording
is saved to your device’s internal storage, not to your computer.
The default settings are to use your device’s standard screen resolution, encode the video at a bitrate of 4Mbps,
and set the maximum screen recording time to 180 seconds. For more information about the command-line
options you can use, run the following command:
adb shell screenrecord –help, This works without rooting the device. Hope this helps.
Section 264.4: Pull (push) files from (to) the device
You may pull (download) files from the device by executing the following command:
adb pull <remote> <local>
For example:
adb pull /sdcard/ ~/
You may also push (upload) files from your computer to the device:
adb push <local> <remote>
For example:
adb push ~/image.jpg /sdcard/
Example to Retrieve Database from device
GoalKicker.com – Android™ Notes for Professionals 1274
sudo adb -d shell "run-as com.example.name cat /data/da/com.example.name /databases/DATABASE_NAME
> /sdcard/file
Section 264.5: Print verbose list of connected devices
To get a verbose list of all devices connected to adb, write the following command in your terminal:
adb devices -l
Example Output
List of devices attached
ZX1G425DC6 device usb:336592896X product:shamu model:Nexus_6 device:shamu
013e4e127e59a868 device usb:337641472X product:bullhead model:Nexus_5X device:bullhead
ZX1D229KCN device usb:335592811X product:titan_retde model:XT1068 device:titan_umtsds
A50PL device usb:331592812X
The first column is the serial number of the device. If it starts with emulator-, this device is an emulator.
usb: the path of the device in the USB subsystem.
product: the product code of the device. This is very manufacturer-specific, and as you can see in the case of
the Archos device A50PL above, it can be blank.
model: the device model. Like product, can be empty.
device: the device code. This is also very manufacturer-specific, and can be empty.
Section 264.6: View logcat
You can run logcat as an adb command or directly in a shell prompt of your emulator or connected device. To view
log output using adb, navigate to your SDK platform-tools/ directory and execute:
$ adb logcat
Alternatively, you can create a shell connection to a device and then execute:
$ adb shell
$ logcat
One useful command is:
adb logcat -v threadtime
This displays the date, invocation time, priority, tag, and the PID and TID of the thread issuing the message in a long
message format.
Filtering
Logcat logs got so called log levels:
V — Verbose, D — Debug, I — Info, W — Warning, E — Error, F — Fatal, S — Silent
You can filter logcat by log level as well. For instance if you want only to output Debug level:
adb logcat *:D
Logcat can be filtered by a package name, of course you can combine it with the log level filter:
GoalKicker.com – Android™ Notes for Professionals 1275
adb logcat <package-name>:<log level>
You can also filter the log using grep (more on filtering logcat output here):
adb logcat | grep <some text>
In Windows, filter can be used using findstr, for example:
adb logcat | findstr <some text>
To view alternative log buffer [main|events|radio], run the logcat with the -b option:
adb logcat -b radio
Save output in file :
adb logcat > logcat.txt
Save output in file while also watching it:
adb logcat | tee logcat.txt
Cleaning the logs:
adb logcat -c
Section 264.7: View and pull cache files of an app
You may use this command for listing the files for your own debuggable apk:
adb shell run-as <sample.package.id> ls /data/data/sample.package.id/cache
And this script for pulling from cache, this copy the content to sdcard first, pull and then remove it at the end:
#!/bin/sh
adb shell "run-as <sample.package.id> cat '/data/data/<sample.package.id>/$1' > '/sdcard/$1'"
adb pull "/sdcard/$1"
adb shell "rm '/sdcard/$1'"
Then you can pull a file from cache like this:
./pull.sh cache/someCachedData.txt
Get Database file via ADB
sudo adb -d shell "run-as com.example.name cat /data/da/com.example.name
/databases/STUDENT_DATABASE > /sdcard/file
Section 264.8: Clear application data
One can clear the user data of a specific app using adb:
adb shell pm clear <package>
GoalKicker.com – Android™ Notes for Professionals 1276
This is the same as to browse the settings on the phone, select the app and press on the clear data button.
pm invokes the package manager on the device
clear deletes all data associated with a package
Section 264.9: View an app's internal data
(data/data/<sample.package.id>) on a device
First, make sure your app can be backed up in AndroidManifest.xml, i.e. android:allowBackup is not false.
Backup command:
adb -s <device_id> backup -noapk <sample.package.id>
Create a tar with dd command:
dd if=backup.ab bs=1 skip=24 | python -c "import
zlib,sys;sys.stdout.write(zlib.decompress(sys.stdin.read()))" > backup.tar
Extract the tar:
tar -xvf backup.tar
You may then view the extracted content.
Section 264.10: Install and run an application
To install an APK file, use the following command:
adb install path/to/apk/file.apk
or if the app is existing and we want to reinstall
adb install -r path/to/apk/file.apk
To uninstall an application, we have to specify its package
adb uninstall application.package.name
Use the following command to start an app with a provided package name (or a specific activity in an app):
adb shell am start -n adb shell am start <package>/<activity>
For example, to start Waze:
adb shell am start -n adb shell am start com.waze/com.waze.FreeMapAppActivity
Section 264.11: Sending broadcast
It's possible to send broadcast to BroadcastReceiver with adb.
In this example we are sending broadcast with action com.test.app.ACTION and string extra in bundle
'foo'='bar':
GoalKicker.com – Android™ Notes for Professionals 1277
adb shell am broadcast -a action com.test.app.ACTION --es foo "bar"
You can put any other supported type to bundle, not only strings:
--ez - boolean
--ei - integer
--el - long
--ef - float
--eu - uri
--eia - int array (separated by ',')
--ela - long array (separated by ',')
--efa - float array (separated by ',')
--esa - string array (separated by ',')
To send intent to specific package/class -n or -p parameter can be used.
Sending to package:
-p com.test.app
Sending to a specific component (SomeReceiver class in com.test.app package):
-n com.test.app/.SomeReceiver
Useful examples:
Sending a "boot complete" broadcast
Sending a "time changed" broadcast after setting time via adb command
Section 264.12: Backup
You can use the adb backup command to backup your device.
adb backup [-f <file>] [-apk|-noapk] [-obb|-noobb] [-shared|-noshared] [-all]
[-system|nosystem] [<packages...>]
-f <filename> specify filename default: creates backup.ab in the current directory
-apk|noapk enable/disable backup of .apks themself default: -noapk
-obb|noobb enable/disable backup of additional files default: -noobb
-shared|noshared backup device's shared storage / SD card contents default: -noshared
-all backup all installed apllications
-system|nosystem include system applications default: -system
<packages> a list of packages to be backed up (e.g. com.example.android.myapp) (not needed if -all is specified)
For a full device backup, including everything, use
adb backup -apk -obb -shared -all -system -f fullbackup.ab
GoalKicker.com – Android™ Notes for Professionals 1278
Note: Doing a full backup can take a long time.
In order to restore a backup, use
adb restore backup.ab
Section 264.13: View available devices
Command:
adb devices
Result example:
List of devices attached
emulator-5554 device
PhoneRT45Fr54 offline
123.454.67.45 no device
First column - device serial number
Second column - connection status
Android documentation
Section 264.14: Connect device by IP
Enter these commands in Android device Terminal
su
setprop service.adb.tcp.port 5555
stop adbd
start adbd
After this, you can use CMD and ADB to connect using the following command
adb connect 192.168.0.101.5555
And you can disable it and return ADB to listening on USB with
setprop service.adb.tcp.port -1
stop adbd
start adbd
From a computer, if you have USB access already (no root required)
It is even easier to switch to using Wi-Fi, if you already have USB. From a command line on the computer that has
the device connected via USB, issue the commands
adb tcpip 5555
adb connect 192.168.0.101:5555
Replace 192.168.0.101 with device IP
GoalKicker.com – Android™ Notes for Professionals 1279
Section 264.15: Install ADB on Linux system
How to install the Android Debugging Bridge (ADB) to a Linux system with the terminal using your distro's
repositories.
Install to Ubuntu/Debian system via apt:
sudo apt-get update
sudo apt-get install adb
Install to Fedora/CentOS system via yum:
sudo yum check-update
sudo yum install android-tools
Install to Gentoo system with portage:
sudo emerge --ask dev-util/android-tools
Install to openSUSE system with zypper:
sudo zypper refresh
sudo zypper install android-tools
Install to Arch system with pacman:
sudo pacman -Syyu
sudo pacman -S android-tools
Section 264.16: View activity stack
adb -s <serialNumber> shell dumpsys activity activities
Very useful when used together with the watch unix command:
watch -n 5 "adb -s <serialNumber> shell dumpsys activity activities | sed -En -e '/Stack #/p' -e
'/Running activities/,/Run #0/p'"
Section 264.17: Reboot device
You can reboot your device by executing the following command:
adb reboot
Perform this command to reboot into bootloader:
adb reboot bootloader
Reboot to recovery mode:
adb reboot recovery
Be aware that the device won't shutdown first!
GoalKicker.com – Android™ Notes for Professionals 1280
Section 264.18: Read device information
Write the following command in your terminal:
adb shell getprop
This will print all available information in the form of key/value pairs.
You can just read specific information by appending the name of a specific key to the command. For example:
adb shell getprop ro.product.model
Here are a few interesting pieces of information that you cat get:
ro.product.model: Model name of the device (e.g. Nexus 6P)
ro.build.version.sdk: API Level of the device (e.g. 23)
ro.product.brand: Branding of the device (e.g. Samsung)
Full Example Output
Section 264.19: List all permissions that require runtime grant
from users on Android 6.0
adb shell pm list permissions -g -d
Section 264.20: Turn on/o Wifi
Turn on:
adb shell svc wifi enable
Turn off:
adb shell svc wifi disable
Section 264.21: Start/stop adb
Start ADB:
adb kill-server
Stop ADB:
adb start-server
GoalKicker.com – Android™ Notes for Professionals 1281
Chapter 265: Localization with resources in
Android
Section 265.1: Configuration types and qualifier names for
each folder under the "res" directory
Each resource directory under the res folder (listed in the example above) can have different variations of the
contained resources in similarly named directory suffixed with different qualifier-values for each
configuration-type.
Example of variations of `` directory with different qualifier values suffixed which are often seen in our android
projects:
drawable/
drawable-en/
drawable-fr-rCA/
drawable-en-port/
drawable-en-notouch-12key/
drawable-port-ldpi/
drawable-port-notouch-12key/
Exhaustive list of all different configuration types and their qualifier values for android resources:
Configuration Qualifier Values
MCC and MNC Examples:
mcc310
mcc310-mnc004
mcc208-mnc00
etc.
Language and region Examples:
en
fr
en-rUS
fr-rFR
fr-rCA
Layout Direction ldrtl
ldltr
smallestWidth swdp
Examples:
sw320dp
sw600dp
sw720dp
Available width wdp
w720dp
w1024dp
Available height hdp
h720dp
h1024dp
Screen size small
GoalKicker.com – Android™ Notes for Professionals 1282
normal
large
xlarge
Screen aspect long
notlong
Round screen round
notround
Screen orientation port
land
UI mode car
desk
television
appliancewatch
Night mode night
notnight
Screen pixel density (dpi) ldpi
mdpi
hdpi
xhdpi
xxhdpi
xxxhdpi
nodpi
tvdpi
anydpi
Touchscreen type notouch
finger
Keyboard availability keysexposed
keyshidden
keyssoft
Primary text input method nokeys
qwerty
12key
Navigation key availability navexposed
navhidden
Primary non-touch navigation method nonav
dpad
trackball
wheel
Platform Version (API level) Examples:
v3
v4
v7
Section 265.2: Adding translation to your Android app
You have to create a different strings.xml file for every new language.
GoalKicker.com – Android™ Notes for Professionals 1283
1. Right-click on the res folder
2. Choose New → Values resource file
3. Select a locale from the available qualifiers
4. Click on the Next button (>>)
5. Select a language
6. Name the file strings.xml
strings.xml
<resources>
<string name="app_name">Testing Application</string>
<string name="hello">Hello World</string>
</resources>
strings.xml(hi)
<resources>
<string name="app_name">परीक्षण आवेदन</string>
<string name="hello">नमस्त ुनि</string>
</resources>
Setting the language programmatically:
public void setLocale(String locale) // Pass "en","hi", etc.
{
myLocale = new Locale(locale);
// Saving selected locale to session - SharedPreferences.
saveLocale(locale);
// Changing locale.
Locale.setDefault(myLocale);
android.content.res.Configuration config = new android.content.res.Configuration();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
config.setLocale(myLocale);
} else {
config.locale = myLocale;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
getBaseContext().createConfigurationContext(config);
} else {
getBaseContext().getResources().updateConfiguration(config,
getBaseContext().getResources().getDisplayMetrics());
}
}
The function above will change the text fields which are referenced from strings.xml. For example, assume that you
have the following two text views:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello"/>
Then, after changing the locale, the language strings having the ids app_name and hello will be changed accordingly.
GoalKicker.com – Android™ Notes for Professionals 1284
Section 265.3: Type of resource directories under the "res"
folder
When localizing different types of resources are required, each of which has its own home in the android project
structure. Following are the different directories that we can place under the \res directory. The resource types
placed in each of these directories are explained in the table below:
Directory Resource Type
animator/ XML files that define property animations.
anim/ XML files that define tween animations. (Property animations can also be saved in this directory, but
the animator/ directory is preferred for property animations to distinguish between the two types.)
color/ XML files that define a state list of colors. See Color State List Resource
drawable/
"Bitmap files (.png, .9.png, .jpg, .gif) or XML files that are compiled into the following drawable resource
subtypes: : Bitmap files - Nine-Patches (re-sizable bitmaps) - State lists - Shapes -
Animation drawables - Other drawables - "
mipmap/ Drawable files for different launcher icon densities. For more information on managing launcher icons
with mipmap/ folders, see Managing Projects Overview.
layout/ XML files that define a user interface layout. See Layout Resource.
menu/ XML files that define application menus, such as an Options Menu, Context Menu, or Sub Menu.
See Menu Resource.
raw/ Arbitrary files to save in their raw form. To open these resources with a raw InputStream,
call Resources.openRawResource() with the resource ID, which is R.raw.filename.
However, if you need access to original file names and file hierarchy, you might consider saving some
resources in the assets/ directory (instead ofres/raw/). Files in assets/ are not given a resource ID, so
you can read them only using AssetManager.
values/ XML files that contain simple values, such as strings, integers, and colors, as well as styles and themes
xml/ Arbitrary XML files that can be read at runtime by calling Resources.getXML(). Various XML configuration
files must be saved here, such as a searchable configuration.
Section 265.4: Change locale of android application
programmatically
In above examples you understand how to localize resources of application. Following example explain how to
change the application locale within application, not from device. In order to change Application locale only, you can
use below locale util.
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.preference.PreferenceManager;
import android.view.ContextThemeWrapper;
import java.util.Locale;
/**
* Created by Umesh on 10/10/16.
*/
public class LocaleUtils {
private static Locale mLocale;
public static void setLocale(Locale locale){
GoalKicker.com – Android™ Notes for Professionals 1285
mLocale = locale;
if(mLocale != null){
Locale.setDefault(mLocale);
}
}
public static void updateConfiguration(ContextThemeWrapper wrapper){
if(mLocale != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
Configuration configuration = new Configuration();
configuration.setLocale(mLocale);
wrapper.applyOverrideConfiguration(configuration);
}
}
public static void updateConfiguration(Application application, Configuration configuration){
if(mLocale != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1){
Configuration config = new Configuration(configuration);
config.locale = mLocale;
Resources res = application.getBaseContext().getResources();
res.updateConfiguration(configuration, res.getDisplayMetrics());
}
}
public static void updateConfiguration(Context context, String language, String country){
Locale locale = new Locale(language,country);
setLocale(locale);
if(mLocale != null){
Resources res = context.getResources();
Configuration configuration = res.getConfiguration();
configuration.locale = mLocale;
res.updateConfiguration(configuration,res.getDisplayMetrics());
}
}
public static String getPrefLangCode(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getString("lang_code","en");
}
public static void setPrefLangCode(Context context, String mPrefLangCode) {
SharedPreferences.Editor editor =
PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putString("lang_code",mPrefLangCode);
editor.commit();
}
public static String getPrefCountryCode(Context context) {
return
PreferenceManager.getDefaultSharedPreferences(context).getString("country_code","US");
}
public static void setPrefCountryCode(Context context,String mPrefCountryCode) {
SharedPreferences.Editor editor =
PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putString("country_code",mPrefCountryCode);
editor.commit();
}
}
GoalKicker.com – Android™ Notes for Professionals 1286
Initialize locale that user preferred, from Application class.
public class LocaleApp extends Application{
@Override
public void onCreate() {
super.onCreate();
LocaleUtils.setLocale(new Locale(LocaleUtils.getPrefLangCode(this),
LocaleUtils.getPrefCountryCode(this)));
LocaleUtils.updateConfiguration(this, getResources().getConfiguration());
}
}
You also need to create a base activity and extend this activity to all other activity so that you can change locale of
application only one place as follows :
public abstract class LocalizationActivity extends AppCompatActivity {
public LocalizationActivity() {
LocaleUtils.updateConfiguration(this);
}
// We only override onCreate
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
Note : Always initialize locale in constructor.
Now you can use LocalizationActivity as follow.
public class MainActivity extends LocalizationActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Note: When you change locale of application programmatically, need to restart your activity to take the
effect of locale change In order to work properly for this solution you and use locale from shared
preferences on app startup you android:name=".LocaleApp" in you Manifest.xml.
Sometimes Lint checker prompt to create the release build. To solve such issue follow below options.
First:
If you want to disable translation for some strings only then add following attribute to default string.xml
GoalKicker.com – Android™ Notes for Professionals 1287
<string name="developer" translatable="false">Developer Name</string>
Second:
Ignore all missing translation from resource file add following attribute It's the ignore attribute of the tools
namespace in your strings file, as follows:
<?xml version="1.0" encoding="utf-8"?>
<resources
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="MissingTranslation" >
http://stackoverflow.com/documentation/android/3345/localization-with-resources-in-android#
<!-- your strings here; no need now for the translatable attribute -->
</resources>
Third:
Another way to disable non-translatable string
http://tools.android.com/recent/non-translatablestrings
If you have a lot of resources that should not be translated, you can place them in a file named donottranslate.xml
and lint will consider all of them non-translatable resources.
Fourth:
You can also add locale in resource file
<resources
xmlns:tools="http://schemas.android.com/tools"
tools:locale="en" tools:ignore="MissingTranslation">
You can also disable missing translation check for lint from app/build.gradle
lintOptions {
disable 'MissingTranslation'
}
Section 265.5: Currency
Currency currency = Currency.getInstance("USD");
NumberFormat format = NumberFormat.getCurrencyInstance();
format.setCurrency(currency);
format.format(10.00);
GoalKicker.com – Android™ Notes for Professionals 1288
Chapter 266: Convert vietnamese string to
english string Android
Section 266.1: example:
String myStr = convert("Lê Minh Thoại là người Việt Nam");
converted:
"Le Minh Thoai la nguoi Viet Nam"
Section 266.2: Chuyển chuỗi Tiếng Việt thành chuỗi không dấu
public static String convert(String str) {
str = str.replaceAll("à|á|ạ|ả|ã|â|ầ|ấ|ậ|ẩ|ẫ|ă|ằ|ắ|ặ|ẳ|ẵ", "a");
str = str.replaceAll("è|é|ẹ|ẻ|ẽ|ê|ề|ế|ệ|ể|ễ", "e");
str = str.replaceAll("ì|í|ị|ỉ|ĩ", "i");
str = str.replaceAll("ò|ó|ọ|ỏ|õ|ô|ồ|ố|ộ|ổ|ỗ|ơ|ờ|ớ|ợ|ở|ỡ", "o");
str = str.replaceAll("ù|ú|ụ|ủ|ũ|ư|ừ|ứ|ự|ử|ữ", "u");
str = str.replaceAll("ỳ|ý|ỵ|ỷ|ỹ", "y");
str = str.replaceAll("đ", "d");
str = str.replaceAll("À|Á|Ạ|Ả|Ã|Â|Ầ|Ấ|Ậ|Ẩ|Ẫ|Ă|Ằ|Ắ|Ặ|Ẳ|Ẵ", "A");
str = str.replaceAll("È|É|Ẹ|Ẻ|Ẽ|Ê|Ề|Ế|Ệ|Ể|Ễ", "E");
str = str.replaceAll("Ì|Í|Ị|Ỉ|Ĩ", "I");
str = str.replaceAll("Ò|Ó|Ọ|Ỏ|Õ|Ô|Ồ|Ố|Ộ|Ổ|Ỗ|Ơ|Ờ|Ớ|Ợ|Ở|Ỡ", "O");
str = str.replaceAll("Ù|Ú|Ụ|Ủ|Ũ|Ư|Ừ|Ứ|Ự|Ử|Ữ", "U");
str = str.replaceAll("Ỳ|Ý|Ỵ|Ỷ|Ỹ", "Y");
str = str.replaceAll("Đ", "D");
return str;
}

Comments

  1. youtube - videodl.cc
    youtube - YouTube - YouTube Video, Videos, News, Pictures, Photos, youtube - YouTube Video, Videos, Videos, Videos, Photos - YouTube Video youtube - YouTube Video, Videos, Videos, Photos, Videos - youtube mp4 YouTube Video

    ReplyDelete

Post a Comment

Popular posts from this blog

Android3?

Android2?