Android TIP: Quick generating Parcelable implement with a plugin in Android Studio

Android TIP: Quick generating Parcelable implement with a plugin in Android Studio

     Along with Serializable, Parcelable is a data type that we usually pass through Intent or Bundle when switching to another Activity or Fragment. This is an interface for classes whose instances can be written to and restored from a Parcel. Parcelable process is much faster than Serializable (a standard interface in JDK). One of the reasons for this is that we are being explicit about the serialization process instead of using reflection to infer it. It also stands to reason that the code has been heavily optimized for this purpose.
    Classes implementing the Parcelable interface must also have a non-null static field called CREATOR of a type that implements the Parcelable.Creator interface. More over, there are some methods you must override:
  • describeContents(): describe the kinds of special objects contained in this Parcelable's marshalled representation. Return a bitmask indicating the set of special object types marshalled by the Parcelable.
  • writeToParcel(): flatten this object in to a Parcel.
  • A constructor with Parcel as the parameter: read all class variables from Parcel.
    As you can see, not simple like implement Serializable, by making a class which implements Parcelable we must write a lot of boring/similar codes (especially your class has so many private variables). So, as a lazy developer like me, you would think to use a generating tool. In this tip, I will present a powerful plugin on IntelliJ/Android Studio to make this work more comfortable and quickly.

Installing the plugin

    Step 1: Go to the plugin release page on Github and download the lastest jar file.
    Step 2: In Android Studio, select menu File -> Settings... (on Windows) or File -> Preferences... (on MacOS), select Plugin entry on the left-side pane. You will have this dialog:
    
    Step 3: Click at "Install plugin from disk" and select the download jar file:
     Step 4: Click "OK", the dialog will be closed and the plugin has installed. This dialog will appear and click "Restart" to restart Android Studio:

Usages in your class

    Just press ALT + Insert (or your equivalent keybinding for code generation) in your editor and select Parcelable. It allows you to select the fields to be parceled:
    And you will have this result:
Customer.java
package info.devexchanges.parcelableplugin;

import android.os.Parcel;
import android.os.Parcelable;

public class Customer implements Parcelable {

    private String name;
    private int id;

    public Customer(String name, int id) {
        this.name = name;
        this.id = id;
    }
    
    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.name);
        dest.writeInt(this.id);
    }

    protected Customer(Parcel in) {
        this.name = in.readString();
        this.id = in.readInt();
    }

    public static final Creator CREATOR = new Creator() {
        @Override
        public Customer createFromParcel(Parcel source) {
            return new Customer(source);
        }

        @Override
        public Customer[] newArray(int size) {
            return new Customer[size];
        }
    };
}
    Now, your work has been done without typing lengthy codes! For more details, please go to the plugin page on Github and read it's documentation! Moreover, check these reference links to understanding Parcelable in Android:
Playing background Audio in Android with MediaSessionCompat

Playing background Audio in Android with MediaSessionCompat

    For a long time I have not write any new tutorial! Today, I come back with talking to a very popular topic: play audio on the background like most of music/audio applications do. When you slide to expand notification area or lock device screen, you still see your custom view of your app (the playing track, pause/stop button, etc...).
   While this is a fairly common feature, it's hard to implement, with lots of different pieces that need to be built correctly in order to give your user the full Android experience. In this tutorial you will learn about MediaSessionCompat from the Android support library, and how it can be used to create a proper background audio service for your users.

Project configuration

    After starting a new Android Studio project, open your AndroidManifest.xml and add WAKE_LOCK permission:
<uses-permission android:name="android.permission.WAKE_LOCK" />
    Next, you will need to declare the use of the MediaButtonReceiver from the Android support library. This will allow you to intercept media control button interactions and headphone events on devices running KitKat and earlier. Add this receiver inside application tag:
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON" />
        <action android:name="android.media.AUDIO_BECOMING_NOISY" />
    </intent-filter>
</receiver>
    Now, copy this MediaStyleHelper.java file (which written by  Ian Lake, Developer Advocate at Google) to your project to clean up the creation of media style notifications:
MediaStyleHelper.java
package com.example.mediasessioncompat;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaButtonReceiver;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.NotificationCompat;

/**
 * Helper APIs for constructing MediaStyle notifications
 */
public class MediaStyleHelper {
    /**
     * Build a notification using the information from the given media session. Makes heavy use
     * of {@link MediaMetadataCompat#getDescription()} to extract the appropriate information.
     * @param context Context used to construct the notification.
     * @param mediaSession Media session to get information.
     * @return A pre-built notification with information from the given media session.
     */
    public static NotificationCompat.Builder from(
            Context context, MediaSessionCompat mediaSession) {
        MediaControllerCompat controller = mediaSession.getController();
        MediaMetadataCompat mediaMetadata = controller.getMetadata();
        MediaDescriptionCompat description = mediaMetadata.getDescription();

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
        builder
                .setContentTitle(description.getTitle())
                .setContentText(description.getSubtitle())
                .setSubText(description.getDescription())
                .setLargeIcon(description.getIconBitmap())
                .setContentIntent(controller.getSessionActivity())
                .setDeleteIntent(
                    MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP))
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
        return builder;
    }
}

Creating the background Audio Service

    In order to build a playback audio streaming service, you must create a subclass of MediaBrowserServiceCompat (I named it is BackgroundAudioService) and implements MediaPlayer.OnCompletionListener and AudioManager.OnAudioFocusChangeListener interfaces.  Firstly, override onGetRoot() and onLoadChildren() with the default code (do nothing):
    @Nullable
    @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
        if(TextUtils.equals(clientPackageName, getPackageName())) {
            return new BrowserRoot(getString(R.string.app_name), null);
        }

        return null;
    }

    @Override
    public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
        result.sendResult(null);
    }
    Next, you must override the onStartCommand() method, which is the entry point into your Service. This method will take the Intent that is passed to the Service and send it to the MediaButtonReceiver class:
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        MediaButtonReceiver.handleIntent(mediaSessionCompat, intent);
        return super.onStartCommand(intent, flags, startId);
    }
    In addition, you should have a BroadcastReceiver that listens for changes in the headphone state. To keep things simple, this receiver will pause the MediaPlayer if it is playing:
private BroadcastReceiver headPhoneReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if( mediaPlayer != null && mediaPlayer.isPlaying() ) {
                mediaPlayer.pause();
            }
        }
    };
    In onCreate() method of this class, initializing a MediaPlayer, a MediaSessionCompat and register the BroadcastReceiver above:
    private MediaPlayer mediaPlayer;
    private MediaSessionCompat mediaSessionCompat;

    @Override
    public void onCreate() {
        super.onCreate();

        initMediaPlayer();
        initMediaSession();
        initNoisyReceiver();
    }

    private void initMediaPlayer() {
        mediaPlayer = new MediaPlayer();
        mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
        mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mediaPlayer.setVolume(1.0f, 1.0f);
    }

    private void initMediaSession() {
        ComponentName mediaButtonReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class);
        mediaSessionCompat = new MediaSessionCompat(getApplicationContext(), "Tag", mediaButtonReceiver, null);

        mediaSessionCompat.setCallback(mediaSessionCallback);
        mediaSessionCompat.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS );

        Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
        mediaButtonIntent.setClass(this, MediaButtonReceiver.class);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0);
        mediaSessionCompat.setMediaButtonReceiver(pendingIntent);

        setSessionToken(mediaSessionCompat.getSessionToken());
    }

    private void initNoisyReceiver() {
        //Handles headphones coming unplugged. cannot be done through a manifest receiver
        IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
        registerReceiver(headPhoneReceiver, filter);
    }
    Now, it's time to look into handling audio focus. Overriding onAudioFocusChange() method with following code:
    @Override
    public void onAudioFocusChange(int focusChange) {
        switch( focusChange ) {
            case AudioManager.AUDIOFOCUS_LOSS: {
                if( mediaPlayer.isPlaying() ) {
                    mediaPlayer.stop();
                }
                break;
            }
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: {
                mediaPlayer.pause();
                break;
            }
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
                if( mediaPlayer != null ) {
                    mediaPlayer.setVolume(0.3f, 0.3f);
                }
                break;
            }
            case AudioManager.AUDIOFOCUS_GAIN: {
                if( mediaPlayer != null ) {
                    if( !mediaPlayer.isPlaying() ) {
                        mediaPlayer.start();
                    }
                    mediaPlayer.setVolume(1.0f, 1.0f);
                }
                break;
            }
        }
    }
    This is explanation about some common states of volume control (in AudioManager class):
  • AUDIOFOCUS_LOSS: used to indicate a loss of audio focus of unknown duration. This occurs when another app has requested audio focus. When this happens, you should stop audio playback in your app.
  • AUDIOFOCUS_LOSS_TRANSIENT: used to indicate a transient loss of audio focus. This state is entered when another app wants to play audio, but it only anticipates needing focus for a short time. You can use this state to pause your audio playback.
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: when audio focus is requested, but throws a 'can duck' state, it means that you can continue your playback, but should bring the volume down a bit. This can occur when a notification sound is played by the device.
  • AUDIOFOCUS_GAIN: this is the state when a duckable audio playback has completed, and your app can resume at its previous levels.
    The last and most important thing you must to is implement a MediaSessionCompat.Callback variable (you've set this callback to mediaSessionCompat through setCallback() method in onCreate() of this class). You must override 3 methods: onPlay(), onPause and onPlayFromMediaId(). This is the code:
private MediaSessionCompat.Callback mediaSessionCallback = new MediaSessionCompat.Callback() {

        @Override
        public void onPlay() {
            super.onPlay();
            if( !successfullyRetrievedAudioFocus() ) {
                return;
            }

            mediaSessionCompat.setActive(true);
            setMediaPlaybackState(PlaybackStateCompat.STATE_PLAYING);

            showPlayingNotification();
            mediaPlayer.start();
        }

        @Override
        public void onPause() {
            super.onPause();

            if( mediaPlayer.isPlaying() ) {
                mediaPlayer.pause();
                setMediaPlaybackState(PlaybackStateCompat.STATE_PAUSED);
                showPausedNotification();
            }
        }

        @Override
        public void onPlayFromMediaId(String mediaId, Bundle extras) {
            super.onPlayFromMediaId(mediaId, extras);

            try {
                AssetFileDescriptor afd = getResources().openRawResourceFd(Integer.valueOf(mediaId));
                if (afd == null) {
                    return;
                }

                try {
                    mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());

                } catch (IllegalStateException e) {
                    mediaPlayer.release();
                    initMediaPlayer();
                    mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
                }

                afd.close();
                initMediaSessionMetadata();

            } catch (IOException e) {
                return;
            }

            try {
                mediaPlayer.prepare();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    };

    private void showPlayingNotification() {
        NotificationCompat.Builder builder = MediaStyleHelper.from(BackgroundAudioService.this, mediaSessionCompat);
        if( builder == null ) {
            return;
        }


        builder.addAction(new NotificationCompat.Action(android.R.drawable.ic_media_pause, "Pause", MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY_PAUSE)));
        builder.setStyle(new NotificationCompat.MediaStyle().setShowActionsInCompactView(0).setMediaSession(mediaSessionCompat.getSessionToken()));
        builder.setSmallIcon(R.mipmap.ic_launcher);
        NotificationManagerCompat.from(BackgroundAudioService.this).notify(1, builder.build());
    }

    private void showPausedNotification() {
        NotificationCompat.Builder builder = MediaStyleHelper.from(this, mediaSessionCompat);
        if( builder == null ) {
            return;
        }

        builder.addAction(new NotificationCompat.Action(android.R.drawable.ic_media_play, "Play", MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY_PAUSE)));
        builder.setStyle(new NotificationCompat.MediaStyle().setShowActionsInCompactView(0).setMediaSession(mediaSessionCompat.getSessionToken()));
        builder.setSmallIcon(R.mipmap.ic_launcher);
        NotificationManagerCompat.from(this).notify(1, builder.build());
    }

    private void setMediaPlaybackState(int state) {
        PlaybackStateCompat.Builder playbackstateBuilder = new PlaybackStateCompat.Builder();
        if( state == PlaybackStateCompat.STATE_PLAYING ) {
            playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PAUSE);
        } else {
            playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY);
        }
        playbackstateBuilder.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 0);
        mediaSessionCompat.setPlaybackState(playbackstateBuilder.build());
    }

    private void initMediaSessionMetadata() {
        MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder();
        //Notification icon in card
        metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
        metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));

        //lock screen icon for pre lollipop
        metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
        metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Beo Dat May Troi");
        metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Singer: Anh Tho");
        metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, 1);
        metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, 1);

        mediaSessionCompat.setMetadata(metadataBuilder.build());
    }

    private boolean successfullyRetrievedAudioFocus() {
        AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);

        int result = audioManager.requestAudioFocus(this,
                AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);

        return result == AudioManager.AUDIOFOCUS_GAIN;
    }
    As you can see on the code above, in the onPlay(), you must check if audio focus was granted first. Below the conditional statement, you will want to set the MediaSessionCompat object to active, give it a state of STATE_PLAYING, and assign the proper actions necessary to create pause buttons on pre-Lollipop lock screen controls. setMediaPlaybackState() method will be called to builds and associates a PlaybackStateCompat with your MediaSessionCompat object.
    Moreover, you must show a playing notification that is associated with your MediaSessionCompat object by using the MediaStyleHelper class that we defined earlier, and then show that notification by call showPlayingNotification() method.
    Finally, you will start the MediaPlayer at the end of onPlay().
    When user click "paused button" button at the notification area, onPause() will be called. Here you will pause the MediaPlayer, set the state to STATE_PAUSED and show a paused notification through call showPausedNotification().
    The next method in the callback is onPlayFromMediaId(), takes a String and a Bundle as parameters. This is the callback method that you can use for changing audio tracks/content within your app. In this tutorial, we will simply accept a raw resource ID (a mp3 file) and attempt to play it.
    There are some optional methods you can override are:
  • onSeekTo(): allows you to change the playback position of your content.
  • onCommand(): allow you to send custom commands to your Service.
    Of course, the last work is handling the audio file has completed playing. Here, you can go to the next track or just simple: release the MediaPlayer:
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
    if( mMediaPlayer != null ) {
        mMediaPlayer.release();
    }
}
    Unregister the BroadcastReceiver, cancel the notification and release MediaSessionCompat in onDestroy():
@Override
public void onDestroy() {
    super.onDestroy();
    AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    audioManager.abandonAudioFocus(this);
    unregisterReceiver(mNoisyReceiver);
    mMediaSessionCompat.release();
    NotificationManagerCompat.from(this).cancel(1);
}
    And never forget to register this Service in your AndroidManifest.xml:
<service android:name=".BackgroundAudioService">
            <intent-filter>
                <action android:name="android.intent.action.MEDIA_BUTTON" />
                <action android:name="android.media.AUDIO_BECOMING_NOISY" />
                <action android:name="android.media.browse.MediaBrowserService" />
            </intent-filter>
        </service>

Creating an Activity to control the media player

    For in-app controls, you always need an Activity. In this activity, you should have a MediaBrowserCompat.ConnectionCallback, MediaControllerCompat.Callback, MediaBrowserCompat and MediaControllerCompat objects created in your app. This is the main activity source code:
MainActivity.java
package info.devexchanges.backgroundaudio;

import android.content.ComponentName;
import android.os.Bundle;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    private static final int STATE_PAUSED = 0;
    private static final int STATE_PLAYING = 1;
    private int currentState;
    private MediaBrowserCompat mediaBrowserCompat;

    private MediaBrowserCompat.ConnectionCallback connectionCallback = new MediaBrowserCompat.ConnectionCallback() {

        @Override
        public void onConnected() {
            super.onConnected();
            try {
                MediaControllerCompat mediaControllerCompat = new MediaControllerCompat(MainActivity.this, mediaBrowserCompat.getSessionToken());
                mediaControllerCompat.registerCallback(mediaControllerCompatCallback);
                setSupportMediaController(mediaControllerCompat);
                getSupportMediaController().getTransportControls().playFromMediaId(String.valueOf(R.raw.beo_dat_may_troi__anh_tho), null);

            } catch( RemoteException e ) {
                e.printStackTrace();
            }
        }
    };

    private MediaControllerCompat.Callback mediaControllerCompatCallback = new MediaControllerCompat.Callback() {

        @Override
        public void onPlaybackStateChanged(PlaybackStateCompat state) {
            super.onPlaybackStateChanged(state);
            if( state == null ) {
                return;
            }

            switch( state.getState() ) {
                case PlaybackStateCompat.STATE_PLAYING: {
                    currentState = STATE_PLAYING;
                    break;
                }
                case PlaybackStateCompat.STATE_PAUSED: {
                    currentState = STATE_PAUSED;
                    break;
                }
            }
        }
    };

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

        Button btnPlay = (Button) findViewById(R.id.btn_play);

        mediaBrowserCompat = new MediaBrowserCompat(this, new ComponentName(this, BackgroundAudioService.class),
                connectionCallback, getIntent().getExtras());
        mediaBrowserCompat.connect();

        btnPlay.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if( currentState == STATE_PAUSED ) {
                    getSupportMediaController().getTransportControls().play();
                    currentState = STATE_PLAYING;
                } else {
                    if( getSupportMediaController().getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING ) {
                        getSupportMediaController().getTransportControls().pause();
                    }

                    currentState = STATE_PAUSED;
                }
            }
        });

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if( getSupportMediaController().getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING ) {
            getSupportMediaController().getTransportControls().pause();
        }

        mediaBrowserCompat.disconnect();
    }
}
    MediaControllerCompat.Callback will have a method called onPlaybackStateChanged() that receives changes in playback state, and can be used to keep your UI in sync.
    MediaBrowserCompat.ConnectionCallback has onConnected() method that will be called when a new MediaBrowserCompat object is created and connected. You can use this to initialize your MediaControllerCompat object, link it to your MediaControllerCompat.Callback, and associate it with MediaSessionCompat from your Service. Once that is completed, you can start audio playback from this method.
    Finally, when your Activity destroyed, you should pause the audio service and disconnect your MediaBrowserCompat object.
    And this is the activity layout (XML file):
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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">

    <Button
        android:id="@+id/btn_play"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Play/Pause Audio" />
</RelativeLayout>
    Running this application, you will see this layout on the notification area while audio is playing on background:
    And if you lock device screen, you still be able to control playback from your app with this "lock screen controls":

Conclusions

    Making an universal playback control on Android devices is a hard topic in application development. Through this post, I hope you can understand the basic way to customizing a layout on the notification area and the lock screen while audio is playing background. For more details, you can go to the official guide of Google developer to find out some interesting features. Finally, please download full project by click the button below, thanks for reading!

References


Androip Tip: create Digital Rain Effect (Matrix Effect)

Androip Tip: create Digital Rain Effect (Matrix Effect)

    You must be familiar with Matrix Effect which has green codes float on black screen (very popular in Matrix film series). In Android, Matrix Effect has been implemented in various applications often as a Live Wallpaper. Through this tip, I would like to present a type of Matrix Effect called Digital Rain Effect - the codes is falling and like rain tears represents the activity of the virtual reality environment.

Custom Matrix effect class

    We will create a custom view based on View class. Firstly, we define some variables for the Digital Rain Effect with the size of the code, the size of a column, the position of the bottom text for each column and then the characters that will be used for the code (I will use only alphabet chars). Note that you can put the characters you want here or why not a custom font:
public class MatrixEffect extends View {

    private static Random random;
    private int width, height;
    private Canvas canvas;
    private Bitmap canvasBmp;
    private int fontSize = 40;
    private int columnSize;
    private char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWSYZabcdefghijklmnopqrstuvwyz".toCharArray();
    private int[] txtPosByColumn;
    private Paint paintTxt, paintBg, paintBgBmp, paintInitBg;
}
    Now, in the constructor, we'll initialize all Paint variables, which define that the text (running code) color is green, the background is black. You also need to create a new Random object here (to random text codes later):
public MatrixEffect(Context context, AttributeSet attrs) {
        super(context, attrs);
        random = new Random();

        paintTxt = new Paint();
        paintTxt.setStyle(Paint.Style.FILL);
        paintTxt.setColor(Color.GREEN);
        paintTxt.setTextSize(fontSize);

        paintBg = new Paint();
        paintBg.setColor(Color.BLACK);
        paintBg.setAlpha(5);
        paintBg.setStyle(Paint.Style.FILL);

        paintBgBmp = new Paint();
        paintBgBmp.setColor(Color.BLACK);

        paintInitBg = new Paint();
        paintInitBg.setColor(Color.BLACK);
        paintInitBg.setAlpha(255);
        paintInitBg.setStyle(Paint.Style.FILL);
    }
    To get view width and height, we override the onSizeChanged() method of the View class. We initialize the position of the first character for each column. We use a random position between the top of the screen and the middle of the screen and save in the txtPosByColumn array:
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;

        canvasBmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        canvas = new Canvas(canvasBmp);
        canvas.drawRect(0, 0, width, height, paintInitBg);
        columnSize = width / fontSize;

        txtPosByColumn = new int[columnSize + 1];

        for (int x = 0; x < columnSize; x++) {
            txtPosByColumn[x] = random.nextInt(height / 2) + 1;
        }
    }
    Finally, we override the onDraw() of our custom view, call the drawCanvas method and invalidate the view to force a redraw. With that call, the Matrix Effect could progress from top to bottom in infinite mode. Inside drawCanvas(), call drawText() method used to draw a random character for each column at the position indicated by txtPosByColumn variable above:
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(canvasBmp, 0, 0, paintBgBmp);
        drawCanvas();
        invalidate();
    }

    private void drawText() {
        for (int i = 0; i < txtPosByColumn.length; i++) {
            canvas.drawText("" + chars[random.nextInt(chars.length)], i * fontSize, txtPosByColumn[i] * fontSize, paintTxt);

            if (txtPosByColumn[i] * fontSize > height && Math.random() > 0.975) {
                txtPosByColumn[i] = 0;
            }
            txtPosByColumn[i]++;
        }
    }

    private void drawCanvas() {
        canvas.drawRect(0, 0, width, height, paintBg);
        drawText();
    }

Usage in Activity/Fragment

    In order to use this view, please declaring a MatrixEffectView in your activity/fragment layout (XML) file:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="info.devexchanges.androidmatrixeffect.MainActivity">

    <info.devexchanges.androidmatrixeffect.MatrixEffectView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>
    And there is no special point in your activity programmatically code, just need setContentView():
MainActivity.java
package info.devexchanges.androidmatrixeffect;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
    You may have this output when running this application:



References: from original post in Ssaurel's blog.
Implement Captcha in Android

Implement Captcha in Android

    Captcha is one of the authentication methods that you are not a robot. Through Wikipedia, a captcha is defined as:
(a backronym for "Completely Automated Public Turing test to tell Computers and Humans Apart") is a type of challenge-response test used in computing to determine whether or not the user is human.
    In web development, this topic is very popular and implement it in a web project is so simple. But in mobile programming in general and Android in particular, implementing captcha is not easy.
    Through this post, I will present the way to build a "captcha bitmap image" which can help you in developing authentication screens.

Create text and math captcha classes

    There are 2 popular types of captcha: text and math operator. We'll create theme and display them as Bitmap. Now, please copy these 3 classes that define the "Bitmap captcha" to your project:
Captcha.java
package info.devexchanges.androidcaptcha;

import java.util.List;
import java.util.Random;

import android.graphics.Bitmap;
import android.graphics.Color;

public abstract class Captcha {
 protected Bitmap image;
 protected String answer = "";
 private int width;
 protected int height;
 protected int x = 0;
 protected int y = 0;
 protected static List usedColors;
 
 protected abstract Bitmap image();

 public static int color(){
     Random r = new Random();
     int number;
     do{
      number = r.nextInt(9);
     }while(usedColors.contains(number));
     usedColors.add(number);
     switch(number){
      case 0: return Color.BLACK;
      case 1: return Color.BLUE;
      case 2: return Color.CYAN;
      case 3: return Color.DKGRAY;
      case 4: return Color.GRAY;
      case 5: return Color.GREEN;
      case 6: return Color.MAGENTA;
      case 7: return Color.RED;
      case 8: return Color.YELLOW;
      case 9: return Color.WHITE;
      default: return Color.WHITE;
     }
    }
    
    public int getWidth(){
     return this.width;
    }
    
    public void setWidth(int width){
     if(width > 0 && width < 10000){
      this.width = width;
     }else{
      this.width = 300;
     }
    }
    
    public int getHeight(){
     return this.height;
    }
    
    public void setHeight(int height){
     if(height > 0 && height < 10000){
      this.height = height;
     }else{
      this.height = 100;
     }
    }
    
 public Bitmap getImage() {
  return this.image;
 }

 public boolean checkAnswer(String ans) {
  return ans.equals(this.answer);
 }
}
TextCaptcha.java
package info.devexchanges.androidcaptcha;

import java.io.CharArrayWriter;
import java.util.ArrayList;
import java.util.Random;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.Bitmap.Config;

public class TextCaptcha extends Captcha {

    protected TextOptions options;
    private int wordLength;
    private char mCh;

    public enum TextOptions {
        UPPERCASE_ONLY,
        LOWERCASE_ONLY,
        NUMBERS_ONLY,
        LETTERS_ONLY,
        NUMBERS_AND_LETTERS
    }

    public TextCaptcha(int wordLength, TextOptions opt) {
        new TextCaptcha(0, 0, wordLength, opt);
    }

    public TextCaptcha(int width, int height, int wordLength, TextOptions opt) {
        setHeight(height);
        setWidth(width);
        this.options = opt;
        usedColors = new ArrayList<>();
        this.wordLength = wordLength;
        this.image = image();
    }

    @Override
    protected Bitmap image() {
        LinearGradient gradient = new LinearGradient(0, 0, getWidth() / this.wordLength, getHeight() / 2, color(), color(), Shader.TileMode.MIRROR);
        Paint p = new Paint();
        p.setDither(true);
        p.setShader(gradient);
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Config.ARGB_8888);
        Canvas c = new Canvas(bitmap);
        c.drawRect(0, 0, getWidth(), getHeight(), p);
        Paint tp = new Paint();
        tp.setDither(true);
        tp.setTextSize(getWidth() / getHeight() * 20);

        Random r = new Random(System.currentTimeMillis());
        CharArrayWriter cab = new CharArrayWriter();
        this.answer = "";
        for (int i = 0; i < this.wordLength; i++) {
            char ch = ' ';
            switch (options) {
                case UPPERCASE_ONLY:
                    ch = (char) (r.nextInt(91 - 65) + (65));
                    break;
                case LOWERCASE_ONLY:
                    ch = (char) (r.nextInt(123 - 97) + (97));
                    break;
                case NUMBERS_ONLY:
                    ch = (char) (r.nextInt(58 - 49) + (49));
                    break;
                case LETTERS_ONLY:
                    ch = getLetters(r);
                    break;
                case NUMBERS_AND_LETTERS:
                    ch = getLettersNumbers(r);
                    break;
                default:
                    ch = getLettersNumbers(r);
                    break;
            }
            cab.append(ch);
            this.answer += ch;
        }

        char[] data = cab.toCharArray();
        for (int i = 0; i < data.length; i++) {
            this.x += (30 - (3 * this.wordLength)) + (Math.abs(r.nextInt()) % (65 - (1.2 * this.wordLength)));
            this.y = 50 + Math.abs(r.nextInt()) % 50;
            Canvas cc = new Canvas(bitmap);
            tp.setTextSkewX(r.nextFloat() - r.nextFloat());
            tp.setColor(color());
            cc.drawText(data, i, 1, this.x, this.y, tp);
            tp.setTextSkewX(0);
        }
        return bitmap;
    }

    private char getLetters(Random r) {
        int rint = (r.nextInt(123 - 65) + (65));
        if (((rint > 90) && (rint < 97)))
            getLetters(r);
        else
            mCh = (char) rint;
        return mCh;
    }

    private char getLettersNumbers(Random r) {
        int rint = (r.nextInt(123 - 49) + (49));

        if (((rint > 90) && (rint < 97)))
            getLettersNumbers(r);
        else if (((rint > 57) && (rint < 65)))
            getLettersNumbers(r);
        else
            mCh = (char) rint;
        return mCh;
    }
}
MathCaptcha.java
package info.devexchanges.androidcaptcha;

import java.util.ArrayList;
import java.util.Random;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;

public class MathCaptcha extends Captcha {
 
 protected MathOptions options;
 
 public enum MathOptions{
  PLUS_MINUS,
  PLUS_MINUS_MULTIPLY
 }
 
 public MathCaptcha(int width, int height, MathOptions opt){
  this.height = height;
     setWidth(width);
     this.options = opt;
  usedColors = new ArrayList<>();
  this.image = image();
 }
 
 @Override
 protected Bitmap image() {
     int one = 0;
     int two = 0;
     int math = 0;

     LinearGradient gradient = new LinearGradient(0, 0, getWidth() / 2, this.height / 2, color(), color(), Shader.TileMode.MIRROR);
     Paint p = new Paint();
     p.setDither(true);
     p.setShader(gradient);
     Bitmap bitmap = Bitmap.createBitmap(getWidth(), this.height, Config.ARGB_8888);
     Canvas c = new Canvas(bitmap);
     c.drawRect(0, 0, getWidth(), this.height, p);
     
     LinearGradient fontGrad = new LinearGradient(0, 0, getWidth() / 2, this.height / 2, color(), color(), Shader.TileMode.CLAMP);
     Paint tp = new Paint();
     tp.setDither(true);
     tp.setShader(fontGrad);
     tp.setTextSize(getWidth() / this.height * 20);
     Random r = new Random(System.currentTimeMillis());
  one = r.nextInt(9) + 1;
  two = r.nextInt(9) + 1;
  math = r.nextInt((options == MathOptions.PLUS_MINUS_MULTIPLY)?3:2);
  if (one < two) {
   Integer temp = one;
   one = two;
   two = temp;
  }
  
  switch (math) {
   case 0:
    this.answer = (one + two) + "";
    break;
   case 1:
    this.answer = (one - two) + "";
       break;
   case 2:
    this.answer = (one * two) + "";
       break;
  }
     char[] data = new char[]{String.valueOf(one).toCharArray()[0], oper(math), String.valueOf(two).toCharArray()[0]};
     for (int i=0; i
    I get these classes from Android-Easy-Captcha project on Github and fix some errors in code!

Usage in Activity/Fragment

    As you can see at the constructors above, we can initialize a new text or math captcha easily by call:
TextCaptcha textCaptcha = new TextCaptcha(600, 150, 4, TextCaptcha.TextOptions.LETTERS_ONLY);
MathCaptcha mathCaptcha = new MathCaptcha(600, 150, MathCaptcha.MathOptions.PLUS_MINUS);
   You will now have a Bitmap with 600x150 pixels. In order to display it to ImageView, only need to call getImage() method (which return a Bitmap object):
imageView.setImageBitmap(textCaptcha.getImage());
imageView1.setImageBitmap(mathCaptcha.getImage());
    The "captcha image" may be look like this:

Check answer from user input

    With checkAnswer() method, we can check whether user input true or false (from EditText) with captcha value:
                //checking text captcha
                if (!textCaptcha.checkAnswer(edtTextCaptcha.getText().toString().trim())) {
                    edtTextCaptcha.setError("Captcha is not match");
                    numberOfCaptchaFalse++;
                } else {
                    Log.d("Main", "captcha is match!");
                }
    Of course, this is the sample output when user input wrong value:
    Important Note: By default, text captcha distinguishes uppercase and lowercase.
    You can make your own output when user input right captcha value, simply showing a Toast like this:

Conclusions

    Now, you've learned the way to create text and math captcha in Android and apply to your own work. Hope this post is helpful with authentication matter in your application. As asual, you can download my sample project by click the button below!

Bottom Navigation View by Design Support Library in Android

    Bottom Navigation View was introduced for a long time ago in Material design guideline but it hasn’t been easy for us to implement it into our apps. Some applications have built their own solutions, whilst others have used a third-party open-source libraries to get the job done. I also had a post about using a library to make this design, you should take a glance here!

    But now, with the release of Design Support Library version 25.0.0, Google now provide an official widget to make this design. This is a good new for Android developers, from now on, we have not depended on any third-party library anymore, just only need to use BottomNavigationView in your layout design.
    DEMO VIDEO:

Adding Design Support Library dependency

    It is included in the Design Support Library, starting with version 25.0.0. You can include it in your build.gradle file with the following line (you'll also need the AppCompat Support Library as the Design Support Library's dependency):
compile 'com.android.support:appcompat-v7:25.0.0'  
compile 'com.android.support:design:25.0.0' 

Declaring in activity layout

    Next we simply need to add the BottomNavigationView widget to our desired layout file. Remember that this should be aligned with the bottom of the screen with all content displaying above it. We can add this view like so:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="info.devexchanges.bottomnavigationview.MainActivity">

    <FrameLayout
        android:id="@+id/fragment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="@dimen/activity_horizontal_margin">

    </FrameLayout>

    <android.support.design.widget.BottomNavigationView
        android:id="@+id/bottom_navigation_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        app:itemBackground="@color/colorPrimaryDark"
        app:itemIconTint="@color/white"
        app:itemTextColor="@color/white"
        app:menu="@menu/menu_bottom_navigation" />
</RelativeLayout>
    As you can see, there are 4 important attributes of BottomNavigationView:
  • itemBackground: the background color or Drawable of the items. Can be set from code with the setItemBackgroundResource() method.
  • itemIconTint: the icon tint for items. Can be set from code with the setItemIconTintList() method.
  • itemTextColor: the text color for the item labels. Can be set from code with the setItemTextColor() method.
  • menu: the menu resource to be used to display items in the bottom navigation menu. Can be set from code with inflateMenu() method.
    If you add this to your app and run it on your device, you’ll see a shiny new bottom navigation view like this:

Create a menu for Bottom Navigation View

    At the XML code above, we provided a menu attribute for our BottomNavigationView. It looks exactly the same as any other menu that we’d use throughout our app, let put a menu resource file in res/menu folder:
menu_bottom_navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/songs"
        android:icon="@drawable/song"
        android:title="All songs" />
    <item
        android:id="@+id/genre"
        android:icon="@drawable/genre"
        android:title="Genres" />
    <item
        android:id="@+id/album"
        android:icon="@drawable/album"
        android:title="Albums" />
    <item
        android:id="@+id/artist"
        android:icon="@drawable/artist"
        android:title="Artists" />
</menu>
    Important Note: the maximum number of items we can display now is 5. You can check it through call getMaxItem() method.

Handle selected/unselected states

    Using the BottomNavigationView we can easily handle the display of both selected and unselected menu items. Firstly, create a selector file for our selected/unselected colors (this file is put in res/color folder):
color_states.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:color="#757575"
        android:state_checked="false"/>
    <item
        android:color="@color/colorAccent"
        android:state_checked="true"/>

</selector>
    Now, change the itemIconTint attribute value of your BottomNavigationView:
<android.support.design.widget.BottomNavigationView
        android:id="@+id/bottom_navigation_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        app:itemBackground="@color/colorPrimaryDark"
        app:itemIconTint="@color/color_states"
        app:itemTextColor="@color/white"
        app:menu="@menu/menu_bottom_navigation" />
    You will have this output:

Handle menu items click event

Now we’ve implemented our menu we need to be able to react when it’s interacted with. In programmatically code, we can use the setOnNavigationItemSelectedListener() method of BottomNavigationView to set a listener for menu item events:
bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                switch (item.getItemId()) {
                    case R.id.artist:

                        break;
                    case R.id.genre:

                        break;
                    case R.id.album:

                        break;
                    case R.id.songs:

                        break;
                    default:
                        break;
                }
                return true;
            }
        });

Conclusions

    I hope you can see now just how straight forward it is to implement the Bottom Navigation view using the design support library. At this time, this widget is still not perfect. For example, we now cannot custom it's behavior to show/hide it when scrolling screen, hope that Google will make it better soon!
    Of course, if you need more code of this project, please check it out on @Github, I will replace corresponding fragments when menu items clicked, output of my sample project like this:
    Read more: