(Tutorial Android) Countdown Timer with Circular Progress Animation

Thursday, August 10, 2017 Add Comment

Countdown merupakan istilah dari bahasa inggris yang artinya hitung mundur. Android menyediakan kelas yang berfungsi untuk menghitung mundur yang dinamakan CountDownTimer. Untuk membuat program countdown, kita tinggal mengimplementasikan kelas tersebut. Pada kesempatan kali ini saya akan membagikan sedikit tutorial hasil dari request teman-teman. Kita akan membuat program countdown dengan animasi circular progress.

Oke langsung saja kita ke TKP.

Pertama, buat project baru (wajib hehehe).

Selanjutnya kita membuat circular progress. Di sini kita harus membuat custom view sendiri untuk membuat circular progress, karena di Android sendiri memang tidak disediakan. Berikut adalah codenya saya ambil dan modifikasi sedikit dari https://github.com/lzyzsd/CircleProgress. Beri nama DonutProgress.java :
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by bruce on 11/4/14.
 */

public class DonutProgress extends View {

    private Paint finishedPaint;
    private Paint unfinishedPaint;
    private Paint innerCirclePaint;

    protected Paint textPaint;
    protected Paint innerBottomTextPaint;

    private RectF finishedOuterRect = new RectF();
    private RectF unfinishedOuterRect = new RectF();

    private int attributeResourceId = 0;
    private boolean showText;
    private float textSize;
    private int textColor;
    private int innerBottomTextColor;
    private int progress = 0;
    private int max;
    private int finishedStrokeColor;
    private int unfinishedStrokeColor;
    private int startingDegree;
    private float finishedStrokeWidth;
    private float unfinishedStrokeWidth;
    private int innerBackgroundColor;
    private String prefixText = "";
    private String suffixText = "";
    private String text = null;
    private float innerBottomTextSize;
    private String innerBottomText;
    private float innerBottomTextHeight;

    private final float default_stroke_width;
    private final int default_finished_color = Color.rgb(66, 145, 241);
    private final int default_unfinished_color = Color.rgb(204, 204, 204);
    private final int default_text_color = Color.rgb(66, 145, 241);
    private final int default_inner_bottom_text_color = Color.rgb(66, 145, 241);
    private final int default_inner_background_color = Color.TRANSPARENT;
    private final int default_max = 100;
    private final int default_startingDegree = 0;
    private final float default_text_size;
    private final float default_inner_bottom_text_size;
    private final int min_size;


    private static final String INSTANCE_STATE = "saved_instance";
    private static final String INSTANCE_TEXT_COLOR = "text_color";
    private static final String INSTANCE_TEXT_SIZE = "text_size";
    private static final String INSTANCE_TEXT = "text";
    private static final String INSTANCE_INNER_BOTTOM_TEXT_SIZE = "inner_bottom_text_size";
    private static final String INSTANCE_INNER_BOTTOM_TEXT = "inner_bottom_text";
    private static final String INSTANCE_INNER_BOTTOM_TEXT_COLOR = "inner_bottom_text_color";
    private static final String INSTANCE_FINISHED_STROKE_COLOR = "finished_stroke_color";
    private static final String INSTANCE_UNFINISHED_STROKE_COLOR = "unfinished_stroke_color";
    private static final String INSTANCE_MAX = "max";
    private static final String INSTANCE_PROGRESS = "progress";
    private static final String INSTANCE_SUFFIX = "suffix";
    private static final String INSTANCE_PREFIX = "prefix";
    private static final String INSTANCE_FINISHED_STROKE_WIDTH = "finished_stroke_width";
    private static final String INSTANCE_UNFINISHED_STROKE_WIDTH = "unfinished_stroke_width";
    private static final String INSTANCE_BACKGROUND_COLOR = "inner_background_color";
    private static final String INSTANCE_STARTING_DEGREE = "starting_degree";
    private static final String INSTANCE_INNER_DRAWABLE = "inner_drawable";

    public DonutProgress(Context context) {
        this(context, null);
    }

    public DonutProgress(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DonutProgress(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        default_text_size = Utils.sp2px(getResources(), 18);
        min_size = (int) Utils.dp2px(getResources(), 100);
        default_stroke_width = Utils.dp2px(getResources(), 10);
        default_inner_bottom_text_size = Utils.sp2px(getResources(), 18);

        final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DonutProgress, defStyleAttr, 0);
        initByAttributes(attributes);
        attributes.recycle();

        initPainters();
    }

    protected void initPainters() {
        if (showText) {
            textPaint = new TextPaint();
            textPaint.setColor(textColor);
            textPaint.setTextSize(textSize);
            textPaint.setAntiAlias(true);

            innerBottomTextPaint = new TextPaint();
            innerBottomTextPaint.setColor(innerBottomTextColor);
            innerBottomTextPaint.setTextSize(innerBottomTextSize);
            innerBottomTextPaint.setAntiAlias(true);
        }

        finishedPaint = new Paint();
        finishedPaint.setColor(finishedStrokeColor);
        finishedPaint.setStyle(Paint.Style.STROKE);
        finishedPaint.setAntiAlias(true);
        finishedPaint.setStrokeWidth(finishedStrokeWidth);

        unfinishedPaint = new Paint();
        unfinishedPaint.setColor(unfinishedStrokeColor);
        unfinishedPaint.setStyle(Paint.Style.STROKE);
        unfinishedPaint.setAntiAlias(true);
        unfinishedPaint.setStrokeWidth(unfinishedStrokeWidth);

        innerCirclePaint = new Paint();
        innerCirclePaint.setColor(innerBackgroundColor);
        innerCirclePaint.setAntiAlias(true);
    }

    protected void initByAttributes(TypedArray attributes) {
        finishedStrokeColor = attributes.getColor(R.styleable.DonutProgress_donut_finished_color, default_finished_color);
        unfinishedStrokeColor = attributes.getColor(R.styleable.DonutProgress_donut_unfinished_color, default_unfinished_color);
        showText = attributes.getBoolean(R.styleable.DonutProgress_donut_show_text, true);
        attributeResourceId = attributes.getResourceId(R.styleable.DonutProgress_donut_inner_drawable, 0);

        setMax(attributes.getInt(R.styleable.DonutProgress_donut_max, default_max));
        setProgress(attributes.getInt(R.styleable.DonutProgress_donut_progress, 0));
        finishedStrokeWidth = attributes.getDimension(R.styleable.DonutProgress_donut_finished_stroke_width, default_stroke_width);
        unfinishedStrokeWidth = attributes.getDimension(R.styleable.DonutProgress_donut_unfinished_stroke_width, default_stroke_width);

        if (showText) {
            if (attributes.getString(R.styleable.DonutProgress_donut_prefix_text) != null) {
                prefixText = attributes.getString(R.styleable.DonutProgress_donut_prefix_text);
            }
            if (attributes.getString(R.styleable.DonutProgress_donut_suffix_text) != null) {
                suffixText = attributes.getString(R.styleable.DonutProgress_donut_suffix_text);
            }
            if (attributes.getString(R.styleable.DonutProgress_donut_text) != null) {
                text = attributes.getString(R.styleable.DonutProgress_donut_text);
            }

            textColor = attributes.getColor(R.styleable.DonutProgress_donut_text_color, default_text_color);
            textSize = attributes.getDimension(R.styleable.DonutProgress_donut_text_size, default_text_size);
            innerBottomTextSize = attributes.getDimension(R.styleable.DonutProgress_donut_inner_bottom_text_size, default_inner_bottom_text_size);
            innerBottomTextColor = attributes.getColor(R.styleable.DonutProgress_donut_inner_bottom_text_color, default_inner_bottom_text_color);
            innerBottomText = attributes.getString(R.styleable.DonutProgress_donut_inner_bottom_text);
        }

        innerBottomTextSize = attributes.getDimension(R.styleable.DonutProgress_donut_inner_bottom_text_size, default_inner_bottom_text_size);
        innerBottomTextColor = attributes.getColor(R.styleable.DonutProgress_donut_inner_bottom_text_color, default_inner_bottom_text_color);
        innerBottomText = attributes.getString(R.styleable.DonutProgress_donut_inner_bottom_text);

        startingDegree = attributes.getInt(R.styleable.DonutProgress_donut_circle_starting_degree, default_startingDegree);
        innerBackgroundColor = attributes.getColor(R.styleable.DonutProgress_donut_background_color, default_inner_background_color);
    }

    @Override
    public void invalidate() {
        initPainters();
        super.invalidate();
    }

    public boolean isShowText() {
        return showText;
    }

    public void setShowText(boolean showText) {
        this.showText = showText;
    }

    public float getFinishedStrokeWidth() {
        return finishedStrokeWidth;
    }

    public void setFinishedStrokeWidth(float finishedStrokeWidth) {
        this.finishedStrokeWidth = finishedStrokeWidth;
        this.invalidate();
    }

    public float getUnfinishedStrokeWidth() {
        return unfinishedStrokeWidth;
    }

    public void setUnfinishedStrokeWidth(float unfinishedStrokeWidth) {
        this.unfinishedStrokeWidth = unfinishedStrokeWidth;
        this.invalidate();
    }

    private float getProgressAngle() {
        return getProgress() / (float) max * 360f;
    }

    public float getProgress() {
        return progress;
    }

    public void setProgress(int progress) {
        this.progress = progress;
        if (this.progress > getMax()) {
            this.progress %= getMax();
        }
        invalidate();
    }

    public int getMax() {
        return max;
    }

    public void setMax(int max) {
        if (max > 0) {
            this.max = max;
            invalidate();
        }
    }

    public float getTextSize() {
        return textSize;
    }

    public void setTextSize(float textSize) {
        this.textSize = textSize;
        this.invalidate();
    }

    public int getTextColor() {
        return textColor;
    }

    public void setTextColor(int textColor) {
        this.textColor = textColor;
        this.invalidate();
    }

    public int getFinishedStrokeColor() {
        return finishedStrokeColor;
    }

    public void setFinishedStrokeColor(int finishedStrokeColor) {
        this.finishedStrokeColor = finishedStrokeColor;
        this.invalidate();
    }

    public int getUnfinishedStrokeColor() {
        return unfinishedStrokeColor;
    }

    public void setUnfinishedStrokeColor(int unfinishedStrokeColor) {
        this.unfinishedStrokeColor = unfinishedStrokeColor;
        this.invalidate();
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
        this.invalidate();
    }

    public String getSuffixText() {
        return suffixText;
    }

    public void setSuffixText(String suffixText) {
        this.suffixText = suffixText;
        this.invalidate();
    }

    public String getPrefixText() {
        return prefixText;
    }

    public void setPrefixText(String prefixText) {
        this.prefixText = prefixText;
        this.invalidate();
    }

    public int getInnerBackgroundColor() {
        return innerBackgroundColor;
    }

    public void setInnerBackgroundColor(int innerBackgroundColor) {
        this.innerBackgroundColor = innerBackgroundColor;
        this.invalidate();
    }


    public String getInnerBottomText() {
        return innerBottomText;
    }

    public void setInnerBottomText(String innerBottomText) {
        this.innerBottomText = innerBottomText;
        this.invalidate();
    }


    public float getInnerBottomTextSize() {
        return innerBottomTextSize;
    }

    public void setInnerBottomTextSize(float innerBottomTextSize) {
        this.innerBottomTextSize = innerBottomTextSize;
        this.invalidate();
    }

    public int getInnerBottomTextColor() {
        return innerBottomTextColor;
    }

    public void setInnerBottomTextColor(int innerBottomTextColor) {
        this.innerBottomTextColor = innerBottomTextColor;
        this.invalidate();
    }

    public int getStartingDegree() {
        return startingDegree;
    }

    public void setStartingDegree(int startingDegree) {
        this.startingDegree = startingDegree;
        this.invalidate();
    }

    public int getAttributeResourceId() {
        return attributeResourceId;
    }

    public void setAttributeResourceId(int attributeResourceId) {
        this.attributeResourceId = attributeResourceId;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measure(widthMeasureSpec), measure(heightMeasureSpec));

        //TODO calculate inner circle height and then position bottom text at the bottom (3/4)
        innerBottomTextHeight = getHeight() - (getHeight() * 3) / 4;
    }

    private int measure(int measureSpec) {
        int result;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = min_size;
            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        float delta = Math.max(finishedStrokeWidth, unfinishedStrokeWidth);
        finishedOuterRect.set(delta,
                delta,
                getWidth() - delta,
                getHeight() - delta);
        unfinishedOuterRect.set(delta,
                delta,
                getWidth() - delta,
                getHeight() - delta);

        float innerCircleRadius = (getWidth() - Math.min(finishedStrokeWidth, unfinishedStrokeWidth) + Math.abs(finishedStrokeWidth - unfinishedStrokeWidth)) / 2f;
        canvas.drawCircle(getWidth() / 2.0f, getHeight() / 2.0f, innerCircleRadius, innerCirclePaint);
        canvas.drawArc(finishedOuterRect, getStartingDegree(), getProgressAngle(), false, finishedPaint);
        canvas.drawArc(unfinishedOuterRect, getStartingDegree() + getProgressAngle(), 360 - getProgressAngle(), false, unfinishedPaint);

        if (showText) {
            String text = this.text != null ? this.text : prefixText + progress + suffixText;
            if (!TextUtils.isEmpty(text)) {
                float textHeight = textPaint.descent() + textPaint.ascent();
                canvas.drawText(text, (getWidth() - textPaint.measureText(text)) / 2.0f, (getWidth() - textHeight) / 2.0f, textPaint);
            }

            if (!TextUtils.isEmpty(getInnerBottomText())) {
                innerBottomTextPaint.setTextSize(innerBottomTextSize);
                float bottomTextBaseline = getHeight() - innerBottomTextHeight - (textPaint.descent() + textPaint.ascent()) / 2;
                canvas.drawText(getInnerBottomText(), (getWidth() - innerBottomTextPaint.measureText(getInnerBottomText())) / 2.0f, bottomTextBaseline, innerBottomTextPaint);
            }
        }

        if (attributeResourceId != 0) {
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), attributeResourceId);
            canvas.drawBitmap(bitmap, (getWidth() - bitmap.getWidth()) / 2.0f, (getHeight() - bitmap.getHeight()) / 2.0f, null);
        }
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        final Bundle bundle = new Bundle();
        bundle.putParcelable(INSTANCE_STATE, super.onSaveInstanceState());
        bundle.putInt(INSTANCE_TEXT_COLOR, getTextColor());
        bundle.putFloat(INSTANCE_TEXT_SIZE, getTextSize());
        bundle.putFloat(INSTANCE_INNER_BOTTOM_TEXT_SIZE, getInnerBottomTextSize());
        bundle.putFloat(INSTANCE_INNER_BOTTOM_TEXT_COLOR, getInnerBottomTextColor());
        bundle.putString(INSTANCE_INNER_BOTTOM_TEXT, getInnerBottomText());
        bundle.putInt(INSTANCE_INNER_BOTTOM_TEXT_COLOR, getInnerBottomTextColor());
        bundle.putInt(INSTANCE_FINISHED_STROKE_COLOR, getFinishedStrokeColor());
        bundle.putInt(INSTANCE_UNFINISHED_STROKE_COLOR, getUnfinishedStrokeColor());
        bundle.putInt(INSTANCE_MAX, getMax());
        bundle.putInt(INSTANCE_STARTING_DEGREE, getStartingDegree());
        bundle.putFloat(INSTANCE_PROGRESS, getProgress());
        bundle.putString(INSTANCE_SUFFIX, getSuffixText());
        bundle.putString(INSTANCE_PREFIX, getPrefixText());
        bundle.putString(INSTANCE_TEXT, getText());
        bundle.putFloat(INSTANCE_FINISHED_STROKE_WIDTH, getFinishedStrokeWidth());
        bundle.putFloat(INSTANCE_UNFINISHED_STROKE_WIDTH, getUnfinishedStrokeWidth());
        bundle.putInt(INSTANCE_BACKGROUND_COLOR, getInnerBackgroundColor());
        bundle.putInt(INSTANCE_INNER_DRAWABLE, getAttributeResourceId());
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            final Bundle bundle = (Bundle) state;
            textColor = bundle.getInt(INSTANCE_TEXT_COLOR);
            textSize = bundle.getFloat(INSTANCE_TEXT_SIZE);
            innerBottomTextSize = bundle.getFloat(INSTANCE_INNER_BOTTOM_TEXT_SIZE);
            innerBottomText = bundle.getString(INSTANCE_INNER_BOTTOM_TEXT);
            innerBottomTextColor = bundle.getInt(INSTANCE_INNER_BOTTOM_TEXT_COLOR);
            finishedStrokeColor = bundle.getInt(INSTANCE_FINISHED_STROKE_COLOR);
            unfinishedStrokeColor = bundle.getInt(INSTANCE_UNFINISHED_STROKE_COLOR);
            finishedStrokeWidth = bundle.getFloat(INSTANCE_FINISHED_STROKE_WIDTH);
            unfinishedStrokeWidth = bundle.getFloat(INSTANCE_UNFINISHED_STROKE_WIDTH);
            innerBackgroundColor = bundle.getInt(INSTANCE_BACKGROUND_COLOR);
            attributeResourceId = bundle.getInt(INSTANCE_INNER_DRAWABLE);
            initPainters();
            setMax(bundle.getInt(INSTANCE_MAX));
            setStartingDegree(bundle.getInt(INSTANCE_STARTING_DEGREE));
            setProgress(bundle.getInt(INSTANCE_PROGRESS));
            suffixText = bundle.getString(INSTANCE_SUFFIX);
            prefixText = bundle.getString(INSTANCE_PREFIX);
            text = bundle.getString(INSTANCE_TEXT);
            super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATE));
            return;
        }
        super.onRestoreInstanceState(state);
    }
    public void setDonut_progress(String percent){
        if(!TextUtils.isEmpty(percent)){
            setProgress(Integer.parseInt(percent));
        }
    }

}

Kemudian kelas Utils.java
import android.content.res.Resources;

/**
 * Created by bruce on 14-11-6.
 */

public final class Utils {

    private Utils() {
    }

    public static float dp2px(Resources resources, float dp) {
        final float scale = resources.getDisplayMetrics().density;
        return  dp * scale + 0.5f;
    }

    public static float sp2px(Resources resources, float sp){
        final float scale = resources.getDisplayMetrics().scaledDensity;
        return sp * scale;
    }

}
Buat layout dengan nama activity_main.xml. Untuk memanggil custom view harus diikuti dengan nama package.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    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="id.co.blogspot.wimsonevel.countdown_animation.MainActivity">

    <id.co.blogspot.wimsonevel.countdown_animation.DonutProgress
        android:id="@+id/countdown_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        custom:donut_progress="10"
        custom:donut_max="10"
        custom:donut_circle_starting_degree="270"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />

    <Button
        android:id="@+id/btn_start"
        android:text="@string/start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/countdown_progress"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="64dp" />

</RelativeLayout>
Tambahkan beberapa resource values berikut :

attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="DonutProgress">
        <attr name="donut_progress" format="integer"/>
        <attr name="donut_max" format="integer"/>

        <attr name="donut_unfinished_color" format="color"/>
        <attr name="donut_finished_color" format="color"/>

        <attr name="donut_finished_stroke_width" format="dimension"/>
        <attr name="donut_unfinished_stroke_width" format="dimension"/>

        <attr name="donut_text_size" format="dimension"/>
        <attr name="donut_text_color" format="color"/>

        <attr name="donut_prefix_text" format="string"/>
        <attr name="donut_suffix_text" format="string"/>
        <attr name="donut_text" format="string"/>

        <attr name="donut_background_color" format="color"/>

        <attr name="donut_inner_bottom_text" format="string"/>
        <attr name="donut_inner_bottom_text_size" format="dimension"/>
        <attr name="donut_inner_bottom_text_color" format="color"/>

        <attr name="donut_circle_starting_degree" format="integer" />
        <attr name="donut_show_text" format="boolean"/>
        <attr name="donut_inner_drawable" format="reference"/>
    </declare-styleable>

</resources>
strings.xml
<resources>
    <string name="app_name">Countdown-Animation</string>
    <string name="start">Start</string>
    <string name="stop">Stop</string>
    <string name="reset">Reset</string>
</resources>
Terakhir implementasikan CountDownTimer di kelas MainActivity.java berikut :
import android.os.CountDownTimer;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private CountDownTimer countDownTimer;
    private DonutProgress countDownProgress;
    private Button btnStart;

    private int status = 0;
    private final long startTime = 10 * 1000;
    private final long interval = 1 * 1000;

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

        countDownProgress = (DonutProgress) findViewById(R.id.countdown_progress);
        btnStart = (Button) findViewById(R.id.btn_start);

        countDownProgress.setProgress(10);

        btnStart.setOnClickListener(this);

        countDownTimer = new CountDownTimer(startTime, interval) {
            @Override
            public void onTick(long millisUntilFinished) {
                countDownProgress.setProgress((int) millisUntilFinished / 1000);
            }

            @Override
            public void onFinish() {
                countDownProgress.setProgress(0);
                status = 2;
                btnStart.setText(R.string.reset);
            }
        };
    }

    @Override
    public void onClick(View view) {
        if (status == 0) {
            countDownTimer.start();
            status = 1;
            btnStart.setText(R.string.stop);
        } else if(status == 1){
            countDownTimer.cancel();
            countDownProgress.setProgress(10);
            status = 0;
            btnStart.setText(R.string.start);
        } else {
            countDownProgress.setProgress(10);
            status = 0;
            btnStart.setText(R.string.start);
        }
    }
}

Build dan jalankan maka hasilnya sebagai berikut :


Source code bisa di download di https://github.com/wimsonevel/Countdown-Animation

Sekian tutorial kali ini semoga bermanfaat.
Jangan lupa share ke sosial media kalian ya :)

(Tutorial Android) Download Image to External Storage using Picasso

Saturday, July 29, 2017 2 Comments

Pada tutorial sebelumnya http://wimsonevel.blogspot.co.id/2016/05/tutorial-android-image-loader-using.html saya menjelaskan bagaimana penggunaan library Picasso sebagai library Image Loader yang berfungsi menampilkan gambar dari penyimpanan lokal maupun link url. Nah, pada kesempatan kali ini saya akan membagikan sedikit code agar gambar yang sudah di-load oleh Picasso bisa didownload atau disimpan di memori penyimpanan.

Langsung aja ke TKP.

Pertama buat project baru agan-agan.

Tambahkan library Picasso di build.gradle
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.squareup.picasso:picasso:2.5.2'
    testCompile 'junit:junit:4.12'
}
Kedua buat layout dengan activity_main.xml dengan komponen Image View dan Button.
<?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"
    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="id.co.blogspot.wimsonevel.android_picassodownload.MainActivity">

    <ImageView
        android:id="@+id/iv_image"
        android:layout_width="300dp"
        android:layout_height="400dp"
        android:layout_gravity="center"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:layout_above="@+id/btn_save"
        tools:ignore="ContentDescription" />

    <Button
        android:id="@+id/btn_save"
        android:text="@string/save_image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true" />

</RelativeLayout>

Kemudian beberapa resouce lainnya berikut :

colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#b94948</color>
    <color name="colorPrimaryDark">#b94948</color>
    <color name="colorAccent">#FF4081</color>
</resources>
strings.xml
<resources>
    <string name="app_name">Android-PicassoDownload</string>

    <string name="save_image">Save Image</string>

</resources>

Picasso Target
Picasso menyediakan sebuat interface Target yang menyediakan callback bitmap sehingga kita dapat memanfaatkan bitmap dari image untuk diproses lebih lanjut.

Contoh penggunaannya :
Picasso.with(this)
        .load(url)
        .into(new Target() {
            @Override
            public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom from) {
                
            }

            @Override
            public void onBitmapFailed(Drawable errorDrawable) {

            }

            @Override
            public void onPrepareLoad(Drawable placeHolderDrawable) {
            }
        });
Hasil bitmap lalu kita proses untuk dijadikan sebuah file image ber-ekstensi jpg. Kemudian disimpan di direktori folder memori eksternal yang kita inginkan. Contohnya saya menyimpan file image di folder Pictures.
File sd = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
File folder = new File(sd, "/PicassoDownload/");
if (!folder.exists()) {
    if (!folder.mkdir()) {
        Log.e("ERROR", "Cannot create a directory!");
    } else {
        folder.mkdir();
    }
}
File fileName = new File(folder, imgName);
try {
    fileName.createNewFile();
    FileOutputStream ostream = new FileOutputStream(fileName);
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, ostream);
    ostream.close();
} catch (Exception e) {
    e.printStackTrace();
}
Buat activity dengan nama MainActivity.java, implementasikan code di atas di dalamnya ketika tombol save diklik. Untuk memodifikasi baik itu membaca maupun menulis file di memori eksternal kita memerlukan permission, di Android versi M ke atas perlu adanya pengecekan permission secara runtime.

Berikut list kode lengkapnya.
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;

import java.io.File;
import java.io.FileOutputStream;

public class MainActivity extends AppCompatActivity {

    private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 100;
    private static final String PREF_CAMERA_REQUESTED = "cameraRequested";

    private String url;

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

        ImageView ivImage= (ImageView) findViewById(R.id.iv_image);
        Button btnSave = (Button) findViewById(R.id.btn_save);

        url = "https://scontent-sit4-1.xx.fbcdn.net/v/t1.0-9/20228885_1440264296060520_6773935769024601349_n.jpg?oh=da445041a4bc99ec499b78d39b8832eb&oe=5A371E89";

        Picasso.with(this)
                .load(url)
                .into(ivImage);

        btnSave.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                saveImage(url, "newfile.jpg");
            }
        });
    }

    private void saveImage(String url, final String imgName) {
        if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
                == PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "In progress...", Toast.LENGTH_SHORT).show();

            Picasso.with(this)
                    .load(url)
                    .into(new Target() {
                        @Override
                        public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom from) {
                            File sd = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
                            File folder = new File(sd, "/PicassoDownload/");
                            if (!folder.exists()) {
                                if (!folder.mkdir()) {
                                    Log.e("ERROR", "Cannot create a directory!");
                                } else {
                                    folder.mkdir();
                                }
                            }

                            File fileName = new File(folder, imgName);

                            try {
                                fileName.createNewFile();
                                FileOutputStream ostream = new FileOutputStream(fileName);
                                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, ostream);
                                ostream.close();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }

                            Toast.makeText(MainActivity.this, "Image Saved Successfully!", Toast.LENGTH_SHORT).show();
                        }

                        @Override
                        public void onBitmapFailed(Drawable errorDrawable) {
                            Toast.makeText(MainActivity.this, "Image Failed to Save", Toast.LENGTH_SHORT).show();
                        }

                        @Override
                        public void onPrepareLoad(Drawable placeHolderDrawable) {

                        }
                    });
        } else {
            requestWriteExternalPermission();
        }
    }

    private void requestWriteExternalPermission() {
        final String[] permissions = new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE};

        if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
            ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE);
        } else {
            if (!isPermissionRequested(PREF_CAMERA_REQUESTED)) {
                ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE);
                setPermissionRequested(PREF_CAMERA_REQUESTED);
            } else {
                Toast.makeText(MainActivity.this, "Please grant storage permission to save images", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void setPermissionRequested(String permission) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = preferences.edit();
        editor.putBoolean(permission, true);
        editor.apply();
    }

    private boolean isPermissionRequested(String permission) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        return preferences.getBoolean(permission, false);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

        switch (requestCode) {
            case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE: {
                if (grantResults.length != 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Log.d("SUCCESS", "Write External permission granted");
                    saveImage(url, "newFile.jpg");
                    return;
                }
                Log.e("ERROR", "Permission not granted: results len = " + grantResults.length +
                        " Result code = " + (grantResults.length > 0 ? grantResults[0] : "(empty)"));
                finish();
            }
            default: {
                Log.d("ERROR", "Got unexpected permission result: " + requestCode);
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
                break;
            }
        }
    }
}
Terakhir tambahkan permission di AndroidManifest.xml.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="id.co.blogspot.wimsonevel.android_picassodownload">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <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>
    </application>

</manifest>
Build dan jalankan maka hasilnya sebagai berikut :


Klik “Save Image” maka image akan tersimpan di folder Pictures->PicassoDownload

Source code lengkap dapat dilihat di https://github.com/wimsonevel/Android-PicassoDownload

Sekian tutorial kali ini dan semoga bermanfaat.
Jangan lupa share ke sosial media kalian ^^

(Tutorial iOS) Custom UITableViewCell in UITableView

Thursday, July 27, 2017 Add Comment

Hallo gaes, sudah lama rasanya tidak menulis tutorial tentang iOS.
Nah pada kesempatan kali ini saya akan coba membahas tentang UITableView. UITableView merupakan widget yang biasa digunakan untuk menampilkan data dalam bentuk list. Pada umumnya komponen di dalam UITableView disebut dengan UITableViewCell. Komponen UITableViewCell inilah yang mengisi setiap baris-baris di dalam list. Secara default, UITabelViewCell cuma berisi title dan subtitle. Maka dari itu jika ingin membuat baris yang lebih komplek maka kita perlu membuat custom UITableViewCell.

Oke singkat penjelasannya di atas, sekarang langsung kita praktekkan aja.

Pertama buat project baru di Xcode, untuk bahasa pemrogramannya menggunakan Swift 3.


Kemudian pada halaman storyboard-nya kita menggunakan Table View Controller. Drag and drop Table View Controller ke halaman storyboard, lalu jadikan sebagai Root View Controller (alias tanda panah berada di Table View Controller).



Setelah itu tambahkan beberapa widget di dalam Table View Cell, contohnya di sini kita menambahkan Image View dan Label. Lalu letakkan dengan posisi sebagai berikut :


Embed Table View Controller tadi ke dalam Navigation Controller dengan cara klik menu EditorEmbed inNavigation Controller





Sekarang kita masuk ke sesi codingnya. Buka file ViewController.swift kemudian implement UITableViewController seperti berikut :
import UIKit

class ViewController: UITableViewController {

    var foods = [Food(thumb: "rendang", name: "Rendang", country: "Indonesia"),
                 Food(thumb: "nasi_goreng", name: "Nasi Goreng", country: "Indonesia"),
                 Food(thumb: "sushi", name: "Sushi", country: "Japan"),
                 Food(thumb: "tom_yum_goong", name: "Tom Yum Goong", country: "Thailand"),
                 Food(thumb: "pad_thai", name: "Pad Thai", country: "Thailand"),
                 Food(thumb: "som_tam", name: "Som Tam", country: "Thailand"),
                 Food(thumb: "dim_sum", name: "Dim Sum", country: "Hongkong"),
                 Food(thumb: "ramen", name: "Ramen", country: "Japan"),
                 Food(thumb: "peking_duck", name: "Peking Duck", country: "China"),
                 Food(thumb: "massaman_curry", name: "Massaman Curry", country: "Thailand")]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}
Tambahkan type struct untuk item Food.
struct Food {
    var thumb = String()
    var name = String()
    var country = String()
}
Tambahkan class CustomCell.
class CustomCell: UITableViewCell {

    @IBOutlet weak var thumbImageView: UIImageView!
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var countryLabel: UILabel!
    
}
Kemudian reference komponen dari Table View Cell di storyboard ke kelas CustomCell di atas.



Tambahkan extension di bawahnya kemudian override beberapa function dari TableView.
extension ViewController {
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return foods.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let food = foods[indexPath.row]
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
        
        cell.thumbImageView.image = UIImage(named: food.thumb)
        
        cell.nameLabel?.text = food.name
        cell.countryLabel.text = food.country
        
        return cell

    }
    
}
Source code lengkapnya jadi sebagai berikut :
import UIKit

class ViewController: UITableViewController {

    var foods = [Food(thumb: "rendang", name: "Rendang", country: "Indonesia"),
                 Food(thumb: "nasi_goreng", name: "Nasi Goreng", country: "Indonesia"),
                 Food(thumb: "sushi", name: "Sushi", country: "Japan"),
                 Food(thumb: "tom_yum_goong", name: "Tom Yum Goong", country: "Thailand"),
                 Food(thumb: "pad_thai", name: "Pad Thai", country: "Thailand"),
                 Food(thumb: "som_tam", name: "Som Tam", country: "Thailand"),
                 Food(thumb: "dim_sum", name: "Dim Sum", country: "Hongkong"),
                 Food(thumb: "ramen", name: "Ramen", country: "Japan"),
                 Food(thumb: "peking_duck", name: "Peking Duck", country: "China"),
                 Food(thumb: "massaman_curry", name: "Massaman Curry", country: "Thailand")]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

struct Food {
    var thumb = String()
    var name = String()
    var country = String()
}

class CustomCell: UITableViewCell {

    @IBOutlet weak var thumbImageView: UIImageView!
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var countryLabel: UILabel!
    
}

extension ViewController {
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return foods.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let food = foods[indexPath.row]
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
        
        cell.thumbImageView.image = UIImage(named: food.thumb)
        
        cell.nameLabel?.text = food.name
        cell.countryLabel.text = food.country
        
        return cell

    }
    
}


Build dan jalankan maka hasilnya seperti ini :


Source code lengkap dapat dilihat di https://github.com/wimsonevel/CustomUITableViewCell

Sekian tutorial dari saya dan semoga bermanfaat.
Jangan lupa share ke sosial media kalian. Thanks :)

(Tutorial Android) Push Notification using Firebase Cloud Messaging (FCM)

Wednesday, July 19, 2017 7 Comments

Firebase Cloud Messaging adalah layanan push notifikasi cross-platform yang dikembangkan oleh Google yang sebelumnya menggantikan GCM, yang memungkinkan kita mengirimkan push notifikasi ke pengguna. Layanan FCM ini sepenuhnya gratis dan tidak dibatasi.

Pada tutorial kali ini saya akan membahas cara mengimplementasikan FCM di Android.

Firebase Cloud Messaging pada umumnya memiliki beberapa tipe notifikasi. Tipe yang akan kita coba pada tutorial ini adalah Notification Message. Tipe notifikasi ini merupakan tipe yang pada umumnya digunakan untuk mengirimkan push notification.

Contoh data yang dikirimkan berupa json sebagai berikut :
{
    "to": "e1w6hEbZn-8:APA91bEUIb2JewYCIiApsMu5JfI5Ak...",
    "notification": {
        "body": "Hello? This is push notification :)",
        "title": "Push Notification",
        "icon": "ic_launcher"
    }
}
Kita dapat juga memilih kemana kita akan mengirimkan push notification tersebut. Misal kita ingin mengirim push notification ke device tertentu bisa menggunakan FCM registration token dari device tersebut. Atau ke segment-segment tertentu bisa dengan mengirimkannya ke topics di firebase.

Itu dia sedikit penjelasannya dari saya. Nah sekarang kita langsung praktek aja.

Sebelumnya buat project baru dulu di Android Studio.

Setelah selesai buat project baru, lalu kita harus login ke firebase console. Silahkan login dulu dengan akun google agan-agan kemudian buka halaman https://firebase.google.com/ lalu klik Go To Console.


Halaman dashboard akan tampak seperti gambar di bawah ini. Untuk membuat project baru klik Add project.


Buat project baru kemudian isi Project name dan Country.


Setelah itu masuk ke menu notification. Di sini ada 2 pilihan platform yang bisa kita pilih, berhubung tutorial kita tentang Android maka kita pilih platform Android.


Setelah itu muncul form untuk mengisi nama package, nickname dan debug certificate SHA-1. Untuk mendapatkan debug certificate, buka terminal atau command prompt dan perintahnya bisa lihat di https://developers.google.com/android/guides/client-auth.


Kemudian download google-services.json lalu letakkan di dalam folder project→app.


Setelah aplikasi di daftarkan di firebase console, kemudian kita masuk ke project baru yang sudah dibuat.

Buka build.gradle pada bagian Top-Level dan tambahkan dependencies berikut.
dependencies {
    ...
    classpath 'com.google.gms:google-services:3.1.0'
    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
}
Lalu build.gradle pada bagian Module-Level tambahkan plugin dan library firebase.
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.0.1'
    compile 'com.google.firebase:firebase-messaging:11.0.2'
    testCompile 'junit:junit:4.12'
}
// Add to the bottom of the file
apply plugin: 'com.google.gms.google-services'
Setelah itu re-build project.

Jika agan menemukan error di bawah, mungkin Google Repository agan belum di update. Silahkan buka SDK Manager lalu update Google Repository.


Sampai di sini kita lanjut ke bagian desain layout.

Buat layout dengan nama activity_main.xml lalu tambahkan kode berikut :
<?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:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="id.co.blogspot.wimsonevel.android_pushnotification.MainActivity">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/firebase_lockup_400"
        tools:ignore="ContentDescription" />

    <Switch
        android:id="@+id/switchNews"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:gravity="start"
        android:checked="false"
        android:text="@string/news"/>

    <Switch
        android:id="@+id/switchPromo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:gravity="start"
        android:checked="false"
        android:text="@string/promo" />

    <Button
        android:id="@+id/btn_token"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="@string/show_token"/>

    <TextView
        android:id="@+id/tv_token"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp" />

</LinearLayout>
Dan beberapa resources lainnya.

strings.xml
<resources>
    <string name="app_name">Android-PushNotification</string>

    <string name="token">Token : %s</string>
    <string name="news">News</string>
    <string name="promo">Promo</string>
    <string name="show_token">Show Token</string>

</resources>
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#FFCC01</color>
    <color name="colorPrimaryDark">#FFA701</color>
    <color name="colorAccent">#FF4081</color>
</resources>
Selanjutnya kita masuk ke bagian coding java-nya. Buat kelas-kelas java di bawah ini :

MyFirebaseInstanceIDService.java
import android.util.Log;

import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.FirebaseInstanceIdService;

/**
 * Created by Wim on 7/15/17.
 */

public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {

    private static final String TAG = "MyFirebaseIIDService";

    @Override
    public void onTokenRefresh() {
        // Get updated InstanceID token.
        String refreshedToken = FirebaseInstanceId.getInstance().getToken();
        Log.d(TAG, "Refreshed token: " + refreshedToken);

        // If you want to send messages to this application instance or
        // manage this apps subscriptions on the server side, send the
        // Instance ID token to your app server.
        sendRegistrationToServer(refreshedToken);
    }

    private void sendRegistrationToServer(String token) {
        // TODO: Implement this method to send token to your app server.
    }

}
Kelas ini merupakan firebase service untuk mendapatkan token unik yang selalu diupdate. Nah, token inilah nantinya yang akan membedakan device satu dengan device lainnya.

MyFirebaseMessagingService.java
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

/**
 * Created by Wim on 7/15/17.
 */

public class MyFirebaseMessagingService extends FirebaseMessagingService {

    private static final String TAG = "MyFirebaseMsgService";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {

        // TODO(developer): Handle FCM messages here.
        Log.d(TAG, "From: " + remoteMessage.getFrom());

        // Check if message contains a data payload.
        if (remoteMessage.getData().size() > 0) {
            Log.d(TAG, "Message data payload: " + remoteMessage.getData());

        }

        // Check if message contains a notification payload.
        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
            sendNotification(remoteMessage.getNotification().getBody());
        }

    }

    private void sendNotification(String messageBody) {
        Intent intent = new Intent(this, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri= RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("FCM Message")
                .setContentText(messageBody)
                .setAutoCancel(true)
                .setSound(defaultSoundUri)
                .setContentIntent(pendingIntent);

        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        notificationManager.notify(0, notificationBuilder.build());
    }

}
Kelas ini berfungsi untuk menerima message dari push notification. Message diterima melalui method onMessageReceived(RemoteMessage remoteMessage).

Lanjutkan dengan membuat kelas activity dengan nama MainActivity.java
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;

import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.messaging.FirebaseMessaging;

public class MainActivity extends AppCompatActivity {

    private static final String PREF_SWITCH_NEWS = "switch_news";
    private static final String PREF_SWITCH_PROMO = "switch_promo";

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


        Switch switchNews = (Switch) findViewById(R.id.switchNews);
        Switch switchPromo = (Switch) findViewById(R.id.switchPromo);
        Button btnToken = (Button) findViewById(R.id.btn_token);

        btnToken.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String token = FirebaseInstanceId.getInstance().getToken();
                TextView tvToken = (TextView) findViewById(R.id.tv_token);
                tvToken.setText(getResources().getString(R.string.token, token));

                Log.i("TOKEN", token);
            }
        });

        switchNews.setChecked(isSwitchChecked(PREF_SWITCH_NEWS));
        switchPromo.setChecked(isSwitchChecked(PREF_SWITCH_PROMO));

        switchNews.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
                setSwitchChecked(PREF_SWITCH_NEWS, checked);

                if(checked) {
                    FirebaseMessaging.getInstance().subscribeToTopic("news");
                    Toast.makeText(getApplicationContext(), "Subscribe to News Topic" , Toast.LENGTH_SHORT).show();
                }else{
                    FirebaseMessaging.getInstance().unsubscribeFromTopic("news");
                    Toast.makeText(getApplicationContext(), "Unsubscribe from News Topic" , Toast.LENGTH_SHORT).show();
                }

            }
        });

        switchPromo.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
                setSwitchChecked(PREF_SWITCH_PROMO, checked);

                if(checked) {
                    FirebaseMessaging.getInstance().subscribeToTopic("promo");
                    Toast.makeText(getApplicationContext(), "Subscribe to Promo Topic", Toast.LENGTH_SHORT).show();
                }else{
                    FirebaseMessaging.getInstance().unsubscribeFromTopic("promo");
                    Toast.makeText(getApplicationContext(), "Unsubscribe from Promo Topic", Toast.LENGTH_SHORT).show();
                }

            }
        });
    }

    private void setSwitchChecked(String permission, boolean isChecked) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = preferences.edit();
        editor.putBoolean(permission, isChecked);
        editor.apply();
    }

    private boolean isSwitchChecked(String permission) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        return preferences.getBoolean(permission, false);
    }
}

Pada kode di atas, untuk men-subscibe atau men-unsubscribe ke topik yang diinginkan tinggal memanggil method :
FirebaseMessaging.getInstance().subscribeToTopic("news");

FirebaseMessaging.getInstance().unsubscribeFromTopic("news");
Terakhir, tambahkan service di AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="id.co.blogspot.wimsonevel.android_pushnotification">

    <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>

        <!-- [START fcm_default_icon] -->
        <!-- Set custom default icon. This is used when no icon is set for incoming notification messages.
             See README(https://goo.gl/l4GJaQ) for more. -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@mipmap/ic_launcher" />
        <!-- Set color used with incoming notification messages. This is used when no color is set for the incoming
             notification message. See README(https://goo.gl/6BKBk7) for more. -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_color"
            android:resource="@color/colorAccent" />
        <!-- [END fcm_default_icon] -->

        <!-- [START firebase_service] -->
        <service
            android:name=".MyFirebaseMessagingService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
            </intent-filter>
        </service>
        <!-- [END firebase_service] -->
        <!-- [START firebase_iid_service] -->
        <service
            android:name=".MyFirebaseInstanceIDService">
            <intent-filter>
                <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
            </intent-filter>
        </service>
        <!-- [END firebase_iid_service] -->

    </application>

</manifest>
Build dan jalankan maka tampilannya seperti berikut.



Push Notification melalui Firebase Console

Sekarang kita akan coba mengirimkan push notification dari firebase console.


Klik Send Message, di aplikasi langsung menerima push notification.


Lalu kita tes juga mengirim push notification ke topik news.


Pada aplikasi, aktifkan pemberitahuan news maka akan mendapatkan push notification di topik news.


Push Notification melalui backend PHP

Selain dari firebase console kita juga dapat mengirimkan push notification dari PHP. Di sini yang dibutuhkan adalah alamat url dan server key. Untuk melihat server key bisa dilihat di menu firebase console bagian settings.


Copy Server key agan, kemudian buat kode php berikut :

index.php
<html>
    <head>
        <title>Firebase Cloud Messaging</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">

        <style>
            input[type=text], select {
                width: 100%;
                padding: 12px 20px;
                margin: 8px 0;
                display: inline-block;
                border: 1px solid #ccc;
                border-radius: 4px;
                box-sizing: border-box;
            }

            input[type=submit] {
                background-color: #4CAF50;
                color: white;
                padding: 14px 20px;
                margin: 8px 0;
                border: none;
                border-radius: 4px;
                cursor: pointer;
            }

            input[type=submit]:hover {
                background-color: #45a049;
            }

        </style>
        
    </head>
    <body>
        <h3>Firebase Cloud Messaging</h3>

        <div>
          <form action="index.php" method="get">
            <label for="token">FCM Registration Token</label>
            <input type="text" id="token" name="token" placeholder="Input Token">

            <label for="message">Message</label>
            <input type="text" id="message" name="message" placeholder="Input Message">
            
            <input type="hidden" name="type" value="single" >
            <input type="submit" value="Kirim">
          </form>
            
            
          <form action="index.php">
            <label for="fname">Topics</label>
              
            <select name="topics">
              <option value="news">News</option>
              <option value="promo">Promo</option>
            </select>

            <label for="lname">Message</label>
            <input type="text" id="message" name="message" placeholder="Input Message">
              
            <input type="hidden" name="type" value="topics" >
            <input type="submit" value="Kirim">
          </form>
            
        </div>

        <?php
        // Enabling error reporting
        error_reporting(-1);
        ini_set('display_errors', 'On');
 
        $type = isset($_GET['type']) ? $_GET['type'] : '';
        
        $fields = NULL;
        
        if($type == "single") {
            $token = isset($_GET['token']) ? $_GET['token'] : '';
            $message = isset($_GET['message']) ? $_GET['message'] : '';
            
            $res = array();
            $res['body'] = $message;
            
            $fields = array(
                'to' => $token,
                'notification' => $res,
            );
        
            echo 'FCM Reg Id : '. $token . '<br/>Message : ' . $message;
        }else if($type == "topics") {
            $topics = isset($_GET['topics']) ? $_GET['topics'] : '';
            $message = isset($_GET['message']) ? $_GET['message'] : '';
            
            $res = array();
            $res['body'] = $message;
            
            $fields = array(
                'to' => '/topics/' . $topics,
                'notification' => $res,
            );
            
            echo json_encode($fields);
            echo 'Topics : '. $topics . '<br/>Message : ' . $message;
        }
        
        // Set POST variables
        $url = 'https://fcm.googleapis.com/fcm/send';
        $server_key = "SERVER KEY AGAN";
        
        $headers = array(
            'Authorization: key=' . $server_key,
            'Content-Type: application/json'
        );
        // Open connection
        $ch = curl_init();
 
        // Set the url, number of POST vars, POST data
        curl_setopt($ch, CURLOPT_URL, $url);
 
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 
        // Disabling SSL Certificate support temporarly
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
 
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
 
        // Execute post
        $result = curl_exec($ch);
        if ($result === FALSE) {
            echo 'Curl failed: ' . curl_error($ch);
        }
 
        // Close connection
        curl_close($ch);
               
        ?>
        
    </body>
</html>
Sekarang silahkan agan coba mengirim push notification. (Tested : 100% work)


Jika ingin mempelajari lebih lanjut silahkan lihat di dokumentasinya di https://firebase.google.com/docs/.

Source code lengkap dapat dilihat di https://github.com/wimsonevel/Android-PushNotification

Sekian dulu tutorial kali ini semoga bermanfaat.
Don’t forget to share, thanks :)

Discover Movie Apps

Saturday, July 15, 2017 Add Comment

Aplikasi untuk mencari dan menemukan informasi dari film-film hollywood terpopuler. Data-data film bersumber dari The Movie Database (TMDb), bisa dilihat juga websitenya https://www.themoviedb.org/. TMDb menyediakan API Public yang dapat digunakan oleh siapa saja untuk mengakses data-data film di dalamnya. So, sejauh ini saya sudah membuat aplikasi berikut yang bisa dikatakan masih sederhana hehehe..

Tampilannya kurang lebih seperti di bawah ini.


Apabila teman-teman tertarik untuk mengoprek, mengutak-atik atau mengembangkan project ini lebih lanjut saya persilahkan langsung aja download atau clone projectnya di github saya.
https://github.com/wimsonevel/Android-MovieDB

Jangan lupa fork dan kasih bintang ya gan :D
Thanks semoga bermanfaat.