+1

The walking step ( Đếm bước chân di chuyển)

Giới thiệu

Các điện thoại thông minh (smartphone) ở phân khúc tầm trung trở lên ngày nay đều có định vị vệ tinh (GPS), lẫn các cảm biến, con quay hồi chuyển, gia tốc kế... nên đo đạc được các vận động cơ thể, và có độ chính xác cao hơn nếu là smartphone cao cấp.

Điều kiện cần đã có, điều kiện đủ là các ứng dụng di động (mobile app) sao cho phù hợp theo nhu cầu sử dụng. Có người chỉ cần đo bước chân đi bộ mỗi ngày, xem tiêu tốn bao nhiêu calori, giảm mỡ được không, có người chi tiết hơn, xem mỗi buổi mình đạp xe dài ngắn ra sao, vận động các bài tập đã đủ tiêu mỡ chưa... theo đó, "app" sẽ chỉ rõ các chỉ số này.

Và trong bài viết này mình sẽ hướng dẫn các bạn cách tạo ra demo 1 ứng dụng như vậy !

1. Công nghệ sử dụng

1.1. Shared Preference

Mình sẽ dùng cách tối ưu Shared Preference cho android như ở bài viết này : Shared Preference

1.2. Config file AndroidManifest.xml

Các permission cần cung cấp cho ứng dụng này :

 <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

    <uses-feature android:name="android.hardware.sensor.accelerometer"/>

Full config

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tuananh.stepdetectorandcounter"
          xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

    <uses-feature android:name="android.hardware.sensor.accelerometer"/>

    <application
        android:name=".App"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".view.activity.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <service
            android:name=".service.StepService"
            android:priority="1000">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <action android:name="android.intent.action.DATE_CHANGED"/>
                <action android:name="android.intent.action.MEDIA_MOUNTED"/>
                <action android:name="android.intent.action.USER_PRESENT"/>
                <action android:name="android.intent.action.ACTION_TIME_TICK"/>
                <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
                <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
            </intent-filter>
        </service>
    </application>
</manifest>

1.3. Sensor

<uses-feature android:name="android.hardware.sensor.accelerometer"/>

2. Các thành phần khác

2.1. Callback update UI

Tạo interface UpdateUiCallBack với function void updateUi(int stepCount);

package com.tuananh.stepdetectorandcounter.step;

/**
 * Created by FRAMGIA\vu.tuan.anh on 21/08/2017.
 */
public interface UpdateUiCallBack {
    void updateUi(int stepCount);
}

2.2. Constant

Key DATE_FORMAT để làm key lưu vào shared preference:

package com.tuananh.stepdetectorandcounter.model;

/**
 * Created by FRAMGIA\vu.tuan.anh on 21/08/2017.
 */
public class Constant {
    public static final String DATE_FORMAT = "yyyy_MM_dd";
}

2.3. CommonUtils

  • Function getKeyToday() lấy key của ngày hôm nay
  • Function getStepNumber() lấy ra số bước chân đã đi của ngày hôm nay
package com.tuananh.stepdetectorandcounter.utils;

import com.tuananh.stepdetectorandcounter.model.Constant;

import java.text.SimpleDateFormat;
import java.util.Calendar;

/**
 * Created by FRAMGIA\vu.tuan.anh on 21/08/2017.
 */
public class CommonUtils {
    public static String getKeyToday() {
        Calendar calendar = Calendar.getInstance();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(Constant.DATE_FORMAT);
        return simpleDateFormat.format(calendar.getTime());
    }

    public static int getStepNumber() {
        return SharedPreferencesUtils.getInstance().get(getKeyToday(), Integer.class);
    }
}

3. StepService

3.1. Khởi tạo notification : initNotification

 private void initNotification() {
        mBuilder = new NotificationCompat.Builder(this);
        mBuilder.setContentTitle(getResources().getString(R.string.app_name))
            .setContentText("The number of steps today: " + mCurrentStep + " step")
            .setContentIntent(getDefaultIntent(Notification.FLAG_ONGOING_EVENT))
            .setWhen(System.currentTimeMillis())
            .setPriority(Notification.PRIORITY_DEFAULT)
            .setAutoCancel(false)
            .setOngoing(true)
            .setSmallIcon(R.mipmap.ic_launcher);
        Notification notification = mBuilder.build();
        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        startForeground(mNotifyIdStep, notification);
    }

3.2. Update notification : updateNotification()

private void updateNotification() {
        Intent hangIntent = new Intent(this, MainActivity.class);
        PendingIntent hangPendingIntent =
            PendingIntent.getActivity(this, 0, hangIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        Notification notification =
            mBuilder.setContentTitle(getResources().getString(R.string.app_name))
                .setContentText("The number of steps today: " + mCurrentStep + " step")
                .setWhen(System.currentTimeMillis())
                .setContentIntent(hangPendingIntent)
                .build();
        mNotificationManager.notify(mNotifyIdStep, notification);
        if (mCallback != null) {
            mCallback.updateUi(mCurrentStep);
        }
    }

3.3. Các action cần xử lý

private void initBroadcastReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        filter.addAction(Intent.ACTION_SHUTDOWN);
        filter.addAction(Intent.ACTION_SCREEN_ON);
        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        filter.addAction(Intent.ACTION_DATE_CHANGED);
        filter.addAction(Intent.ACTION_TIME_CHANGED);
        filter.addAction(Intent.ACTION_TIME_TICK);
        mBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                switch (action) {
                    case Intent.ACTION_SCREEN_ON:
                        Log.i(TAG, "screen_on");
                        break;
                    case Intent.ACTION_SCREEN_OFF:
                        Log.i(TAG, "screen_off");
                        break;
                    case Intent.ACTION_USER_PRESENT:
                        Log.i(TAG, "screen unlock");
                        break;
                    case Intent.ACTION_CLOSE_SYSTEM_DIALOGS:
                        Log.i(TAG, "receive ACTION_CLOSE_SYSTEM_DIALOGS");
                        saveData();
                        break;
                    case Intent.ACTION_SHUTDOWN:
                        Log.i(TAG, "receive ACTION_SHUTDOWN");
                        saveData();
                        break;
                    case Intent.ACTION_DATE_CHANGED:
                        Log.i(TAG, "receive ACTION_DATE_CHANGED");
                        saveData();
                        break;
                    case Intent.ACTION_TIME_CHANGED:
                        Log.i(TAG, "receive ACTION_TIME_CHANGED");
                        saveData();
                        break;
                    case Intent.ACTION_TIME_TICK:
                        Log.i(TAG, "receive ACTION_TIME_TICK");
                        saveData();
                        break;
                }
            }
        };
        registerReceiver(mBroadcastReceiver, filter);
    }

3.4. Khởi tạo số bước của ngày hiện tại

 private void initTodayData() {
        mCurrentStep = CommonUtils.getStepNumber();
        updateNotification();
    }

3.5. Check API hỗ trợ

private void startStepDetector() {
        if (mSensorManager != null) {
            mSensorManager = null;
        }
        mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        int VERSION_CODES = Build.VERSION.SDK_INT;
        if (VERSION_CODES >= 19) {
            addCountStepListener();
        } else {
            addBasePedometerListener();
        }
    }

  • Đăng ký listener cho SensorManager:
private void addCountStepListener() {
        Sensor countSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
        Sensor detectorSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR);
        if (countSensor != null) {
            mStepSensorType = Sensor.TYPE_STEP_COUNTER;
            Log.v(TAG, "Sensor.TYPE_STEP_COUNTER");
            mSensorManager
                .registerListener(StepService.this, countSensor, SensorManager.SENSOR_DELAY_NORMAL);
        } else if (detectorSensor != null) {
            mStepSensorType = Sensor.TYPE_STEP_DETECTOR;
            Log.v(TAG, "Sensor.TYPE_STEP_DETECTOR");
            mSensorManager.registerListener(StepService.this, detectorSensor,
                SensorManager.SENSOR_DELAY_NORMAL);
        } else {
            Log.v(TAG, "Count sensor not available!");
            addBasePedometerListener();
        }
    }

3.6. SensorEventListener

public class StepService extends Service implements SensorEventListener {}
  • Nếu là lần đầu tiên thì cho mHasStepCount = tempStep với : int tempStep = (int) sensorEvent.values[0];
 if (!mHasRecord) {
                    mHasRecord = true;
                    mHasStepCount = tempStep;
                } 
  • Full code
@Override
    public void onSensorChanged(SensorEvent sensorEvent) {
        switch (mStepSensorType) {
            case Sensor.TYPE_STEP_COUNTER:
                int tempStep = (int) sensorEvent.values[0];
                Log.d(TAG, "tempStep = " + tempStep);
                if (!mHasRecord) {
                    mHasRecord = true;
                    mHasStepCount = tempStep;
                } else {
                    int thisStepCount = tempStep - mHasStepCount;
                    int thisStep = thisStepCount - mPreviousStepCount;
                    mCurrentStep += thisStep;
                    mPreviousStepCount = thisStepCount;
                }
                break;
            case Sensor.TYPE_STEP_DETECTOR:
                if (sensorEvent.values[0] == 1.0) {
                    mCurrentStep++;
                }
                break;
        }
        updateNotification();
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {
    }

3.7. Full code

package com.tuananh.stepdetectorandcounter.service;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.tuananh.stepdetectorandcounter.R;
import com.tuananh.stepdetectorandcounter.step.UpdateUiCallBack;
import com.tuananh.stepdetectorandcounter.utils.CommonUtils;
import com.tuananh.stepdetectorandcounter.utils.SharedPreferencesUtils;
import com.tuananh.stepdetectorandcounter.view.activity.MainActivity;

/**
 * Created by FRAMGIA\vu.tuan.anh on 21/08/2017.
 */
public class StepService extends Service implements SensorEventListener {
    private static final String TAG = "TAG: " + StepService.class.getSimpleName();
    private static int mStepSensorType = -1;
    private UpdateUiCallBack mCallback;
    private NotificationManager mNotificationManager;
    private NotificationCompat.Builder mBuilder;
    private BroadcastReceiver mBroadcastReceiver;
    private StepBinder mStepBinder = new StepBinder();
    private SensorManager mSensorManager;
    private int mCurrentStep;
    private int mNotifyIdStep = 100;
    private int mHasStepCount = 0;
    private int mPreviousStepCount = 0;
    private boolean mHasRecord;

    @Override
    public void onCreate() {
        super.onCreate();
        initNotification();
        initTodayData();
        initBroadcastReceiver();
        new Thread(new Runnable() {
            public void run() {
                startStepDetector();
            }
        }).start();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mStepBinder;
    }

    public void registerCallback(UpdateUiCallBack paramICallback) {
        mCallback = paramICallback;
    }

    private void startStepDetector() {
        if (mSensorManager != null) {
            mSensorManager = null;
        }
        mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        int VERSION_CODES = Build.VERSION.SDK_INT;
        if (VERSION_CODES >= 19) {
            addCountStepListener();
        } else {
            addBasePedometerListener();
        }
    }

    private void addCountStepListener() {
        Sensor countSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
        Sensor detectorSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR);
        if (countSensor != null) {
            mStepSensorType = Sensor.TYPE_STEP_COUNTER;
            Log.v(TAG, "Sensor.TYPE_STEP_COUNTER");
            mSensorManager
                .registerListener(StepService.this, countSensor, SensorManager.SENSOR_DELAY_NORMAL);
        } else if (detectorSensor != null) {
            mStepSensorType = Sensor.TYPE_STEP_DETECTOR;
            Log.v(TAG, "Sensor.TYPE_STEP_DETECTOR");
            mSensorManager.registerListener(StepService.this, detectorSensor,
                SensorManager.SENSOR_DELAY_NORMAL);
        } else {
            Log.v(TAG, "Count sensor not available!");
            addBasePedometerListener();
        }
    }

    private void addBasePedometerListener() {
        // TODO: 23/08/2017
    }

    public int getStepCount() {
        return mCurrentStep;
    }

    public PendingIntent getDefaultIntent(int flags) {
        return PendingIntent.getActivity(this, 1, new Intent(), flags);
    }

    private void initTodayData() {
        mCurrentStep = CommonUtils.getStepNumber();
        updateNotification();
    }

    private void initBroadcastReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        filter.addAction(Intent.ACTION_SHUTDOWN);
        filter.addAction(Intent.ACTION_SCREEN_ON);
        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        filter.addAction(Intent.ACTION_DATE_CHANGED);
        filter.addAction(Intent.ACTION_TIME_CHANGED);
        filter.addAction(Intent.ACTION_TIME_TICK);
        mBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                switch (action) {
                    case Intent.ACTION_SCREEN_ON:
                        Log.i(TAG, "screen_on");
                        break;
                    case Intent.ACTION_SCREEN_OFF:
                        Log.i(TAG, "screen_off");
                        break;
                    case Intent.ACTION_USER_PRESENT:
                        Log.i(TAG, "screen unlock");
                        break;
                    case Intent.ACTION_CLOSE_SYSTEM_DIALOGS:
                        Log.i(TAG, "receive ACTION_CLOSE_SYSTEM_DIALOGS");
                        saveData();
                        break;
                    case Intent.ACTION_SHUTDOWN:
                        Log.i(TAG, "receive ACTION_SHUTDOWN");
                        saveData();
                        break;
                    case Intent.ACTION_DATE_CHANGED:
                        Log.i(TAG, "receive ACTION_DATE_CHANGED");
                        saveData();
                        break;
                    case Intent.ACTION_TIME_CHANGED:
                        Log.i(TAG, "receive ACTION_TIME_CHANGED");
                        saveData();
                        break;
                    case Intent.ACTION_TIME_TICK:
                        Log.i(TAG, "receive ACTION_TIME_TICK");
                        saveData();
                        break;
                }
            }
        };
        registerReceiver(mBroadcastReceiver, filter);
    }

    private void initNotification() {
        mBuilder = new NotificationCompat.Builder(this);
        mBuilder.setContentTitle(getResources().getString(R.string.app_name))
            .setContentText("The number of steps today: " + mCurrentStep + " step")
            .setContentIntent(getDefaultIntent(Notification.FLAG_ONGOING_EVENT))
            .setWhen(System.currentTimeMillis())
            .setPriority(Notification.PRIORITY_DEFAULT)
            .setAutoCancel(false)
            .setOngoing(true)
            .setSmallIcon(R.mipmap.ic_launcher);
        Notification notification = mBuilder.build();
        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        startForeground(mNotifyIdStep, notification);
    }

    private void updateNotification() {
        Intent hangIntent = new Intent(this, MainActivity.class);
        PendingIntent hangPendingIntent =
            PendingIntent.getActivity(this, 0, hangIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        Notification notification =
            mBuilder.setContentTitle(getResources().getString(R.string.app_name))
                .setContentText("The number of steps today: " + mCurrentStep + " step")
                .setWhen(System.currentTimeMillis())
                .setContentIntent(hangPendingIntent)
                .build();
        mNotificationManager.notify(mNotifyIdStep, notification);
        if (mCallback != null) {
            mCallback.updateUi(mCurrentStep);
        }
    }

    @Override
    public void onSensorChanged(SensorEvent sensorEvent) {
        switch (mStepSensorType) {
            case Sensor.TYPE_STEP_COUNTER:
                int tempStep = (int) sensorEvent.values[0];
                Log.d(TAG, "tempStep = " + tempStep);
                if (!mHasRecord) {
                    mHasRecord = true;
                    mHasStepCount = tempStep;
                } else {
                    int thisStepCount = tempStep - mHasStepCount;
                    int thisStep = thisStepCount - mPreviousStepCount;
                    mCurrentStep += thisStep;
                    mPreviousStepCount = thisStepCount;
                }
                break;
            case Sensor.TYPE_STEP_DETECTOR:
                if (sensorEvent.values[0] == 1.0) {
                    mCurrentStep++;
                }
                break;
        }
        updateNotification();
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {
    }

    public void saveData() {
        SharedPreferencesUtils.getInstance().put(CommonUtils.getKeyToday(), mCurrentStep);
    }

    public class StepBinder extends Binder {
        public StepService getService() {
            return StepService.this;
        }
    }
}

4. MainActivity

4.1 Layout activity_main

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.tuananh.stepdetectorandcounter.view.activity.MainActivity">

        <TextView
            android:id="@+id/text_step"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:gravity="center_horizontal"
            android:text="0"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="50sp"/>

    </LinearLayout>
</layout>

4.2 MainActivity

  • stepService.registerCallback đăng kí call back trả về số bước chân
  • Function updateUi(int stepCount) trả về số bước chân đã đi từ stepService
   stepService.registerCallback(new UpdateUiCallBack() {
                    @Override
                    public void updateUi(int stepCount) {
                        showStepCount(CommonUtils.getStepNumber(), stepCount);
                    }
                });
  • Khởi tạo service connection và trong onServiceConnected sẽ cập nhật số bước chân
 private ServiceConnection mServiceConnection = new
        ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder service) {
                StepService stepService = ((StepService.StepBinder) service).getService();
                showStepCount(CommonUtils.getStepNumber(),
                    stepService.getStepCount());
                stepService.registerCallback(new UpdateUiCallBack() {
                    @Override
                    public void updateUi(int stepCount) {
                        showStepCount(CommonUtils.getStepNumber(), stepCount);
                    }
                });
            }

            @Override
            public void onServiceDisconnected(ComponentName componentName) {
            }
        };
  • Hiển thị số bước chân đã đi lên màn hình showStepCount(int totalStepNum, int currentCounts)
   public void showStepCount(int totalStepNum, int currentCounts) {
        if (currentCounts < totalStepNum) {
            currentCounts = totalStepNum;
        }
        mBinding.textStep.setText(String.valueOf(currentCounts));
    }
  • Khởi tạo service đếm bước chân
private void setupService() {
        Intent intent = new Intent(this, StepService.class);
        mIsBind = bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
        startService(intent);
    }
  • Hủy service
  @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mIsBind) {
            unbindService(mServiceConnection);
        }
    }
  • Khởi tạo data
  private void initData() {
        showStepCount(CommonUtils.getStepNumber(), 0);
        setupService();
    }
  • Full code
package com.tuananh.stepdetectorandcounter.view.activity;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;

import com.tuananh.stepdetectorandcounter.R;
import com.tuananh.stepdetectorandcounter.databinding.ActivityMainBinding;
import com.tuananh.stepdetectorandcounter.service.StepService;
import com.tuananh.stepdetectorandcounter.step.UpdateUiCallBack;
import com.tuananh.stepdetectorandcounter.utils.CommonUtils;

public class MainActivity extends AppCompatActivity {
    private boolean mIsBind;
    private ActivityMainBinding mBinding;
    private ServiceConnection mServiceConnection = new
        ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder service) {
                StepService stepService = ((StepService.StepBinder) service).getService();
                showStepCount(CommonUtils.getStepNumber(),
                    stepService.getStepCount());
                stepService.registerCallback(new UpdateUiCallBack() {
                    @Override
                    public void updateUi(int stepCount) {
                        showStepCount(CommonUtils.getStepNumber(), stepCount);
                    }
                });
            }

            @Override
            public void onServiceDisconnected(ComponentName componentName) {
            }
        };

    public void showStepCount(int totalStepNum, int currentCounts) {
        if (currentCounts < totalStepNum) {
            currentCounts = totalStepNum;
        }
        mBinding.textStep.setText(String.valueOf(currentCounts));
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        initData();
    }

    private void initData() {
        showStepCount(CommonUtils.getStepNumber(), 0);
        setupService();
    }

    private void setupService() {
        Intent intent = new Intent(this, StepService.class);
        mIsBind = bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
        startService(intent);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mIsBind) {
            unbindService(mServiceConnection);
        }
    }
}

Image

Notification

Number step

Resource

Resource


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí