메뉴 건너뛰기

모바일앱


심박수를 측정할 수 있는 센서가 부착된 안드로이드 스마트폰에서 심박수를 측정하는 코드 예제 입니다.


1. 가장 먼저 Google Developer Console 에서 Fitness API 사용을 활성화 합니다.

- 기존에 만든 프로젝트가 없다면 프로젝트를 생성합니다.

- 생성한 프로젝트에서 좌측의 "라이브러리"를 클릭합니다.

- Fitness API 를 검색합니다. 그리고 '사용설정'을 클릭합니다.


2. Google Developer Console 의 촤측에서 '사용자 인증정보'를 클릭합니다.

3. '사용자 인증정보 만들기'를 클릭하여 "OAuth 클라이언트 ID"를 선택합니다.


4. 안드로이드를 선택하면 서명 인증서 지문과 패키지이름을 입력하는 칸이 나타납니다.

- 아래와 같이 '디버그용 키스토어'의 인증서 지문을 얻어 옵니다. 

keytool -keystore ~/.android/debug.keystore -list -v

- 만약 릴리즈 바이너리용으로 제작하고 싶으시다면 상기의 'debug.keystore' 파일 대신 릴리즈용 키스토어 파일의 이름을 입력합니다

- 상기 내용은 Mac의 콘솔기준입니다.

- 패스워드를 물어보면 그냥 엔터를 입력합니다.

- 그리고 'SHA1' 이후의 값들을 긁어서 Google Developer Console 에 보이는 '서명 인증서 지문'란에 입력합니다.

- 패키지 이름을 입력한 후 '생성' 버튼을 클릭합니다.


5. 안드로이드의 'Androidmanifest.xml' 파일에 아래의 필요 권한들을 입력합니다.

<uses-permission android:name="android.permission.BODY_SENSORS" />
<uses-permission android:name="android.permission.FITNESS_BODY_READ" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

6. Logcat으로 3초마다 심작박동 수를 출력하는 Activity 코드입니다.
package org.airpage.heartbeat;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Build;
import android.os.PowerManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
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.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.Scopes;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Scope;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.fitness.Fitness;
import com.google.android.gms.fitness.data.DataPoint;
import com.google.android.gms.fitness.data.DataSource;
import com.google.android.gms.fitness.data.DataType;
import com.google.android.gms.fitness.data.Field;
import com.google.android.gms.fitness.data.Value;
import com.google.android.gms.fitness.request.DataSourcesRequest;
import com.google.android.gms.fitness.request.OnDataPointListener;
import com.google.android.gms.fitness.request.SensorRequest;
import com.google.android.gms.fitness.result.DataSourcesResult;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity {

    private String TAG = MainActivity.class.getName();
    private GoogleApiClient googleApiClient;
    private boolean authInProgress = false;
    private OnDataPointListener onDataPointListener;
    private static final int AUTH_REQUEST = 1;

    private static final String[] REQUIRED_PERMISSION_LIST = new String[]{
            Manifest.permission.BODY_SENSORS
    };

    private static final int REQUEST_PERMISSION_CODE = 12345;
    private List<String> missingPermission = new ArrayList<>();

    private boolean bCheckStarted = false;
    private boolean bGoogleConnected = false;

    private Button btnStart;
    private ProgressBar spinner;
    private PowerManager powerManager;
    private PowerManager.WakeLock wakeLock;
    private TextView textMon;



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

        //심박패턴을 측정하는 동안 화면이 꺼지지 않도록 제어하기 위해 전원관리자를 얻어옵니다
        powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, "WAKELOCK");
        initUI();
        //필요한 권한을 얻었는지 확인하고, 얻지 않았다면 권한 요청을 하기 위한 코드를 호출합니다
        checkAndRequestPermissions();
    }

    private void initUI() {
        //심박수를 측정하는 Google API의 호출을 위해 API 클라이언트를 초기화 합니다
        initGoogleApiClient();

        textMon = findViewById(R.id.textMon);
        spinner = findViewById(R.id.progressBar1);
        spinner.setVisibility(View.INVISIBLE);
        btnStart = findViewById(R.id.btnStart);
        btnStart.setText("Wait please ...");
        btnStart.setEnabled(false);

        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (bCheckStarted) {
                    //btnStart.setText(R.string.msg_start);
                    btnStart.setText("Start");
                    bCheckStarted = false;

                    unregisterFitnessDataListener();

                    spinner.setVisibility(View.INVISIBLE);

                    wakeLock.release();
                }
                else {
                    //버튼을 처음 클릭할 경우 Google API 클라이언트에 로그인이 되어있는 상태인지를 확인합니다.
                    //만약 로그인이 되어 있는 상태라면,
                    if (bGoogleConnected == true) {
                        //심박수를 측정하기 위한 API를 설정합니다
                        findDataSources();
                        //심박수의 측정이 시작되면 심박수 정보를 얻을 콜백함수를 등록/설정하는 함수를 호출합니다
                        registerDataSourceListener(DataType.TYPE_HEART_RATE_BPM);
                        btnStart.setText("Stop");
                        //btnStart.setText(R.string.msg_stop);
                        bCheckStarted = true;

                        spinner.setVisibility(View.VISIBLE);
                        //화면이 꺼지지 않도록 설정합니다
                        wakeLock.acquire();

                    }
                    // Google API 클라이언트에 로그인이 되어 있지 않다면,
                    else { 
                        //Google API 클라이언트에 로그인 합니다
                        if (MainActivity.this.googleApiClient != null)
                            MainActivity.this.googleApiClient.connect();
                    }
                }
            }
        });
    }

    private void initGoogleApiClient() {
        this.googleApiClient = new GoogleApiClient.Builder(this)
                .addApi(Fitness.SENSORS_API)
                .addScope(new Scope(Scopes.FITNESS_BODY_READ))
                //.addScope(new Scope(Scopes.FITNESS_ACTIVITY_READ))
                .addConnectionCallbacks(
                        new GoogleApiClient.ConnectionCallbacks() {

                            //Google API 클라이언트의 로그인에 성공하면 호출이 되는 콜백입니다
                            @Override
                            public void onConnected(Bundle bundle) {
                                Log.d(TAG, "initGoogleApiClient() onConnected good...");
                                bGoogleConnected = true;
                                btnStart.setText("Start");
                                btnStart.setEnabled(true);
                            }

                            @Override
                            public void onConnectionSuspended(int i) {

                                if (i == GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST) {
                                    Log.d(TAG, "onConnectionSuspended() network_lost bad...");
                                } else if (i == GoogleApiClient.ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED) {
                                    Log.d(TAG, "onConnectionSuspended() service_disconnected bad...");

                                }
                            }
                        }
                )
                .addOnConnectionFailedListener(
                        new GoogleApiClient.OnConnectionFailedListener() {

                            @Override
                            public void onConnectionFailed(ConnectionResult result) {
                                Log.d(TAG, "Connection failed. Cause: " + result.toString());

                                if (!result.hasResolution()) {
                                    MainActivity.this.finish();
                                    return;
                                }

                                if (!authInProgress) {
                                    try {
                                        Log.d(TAG, "Attempting to resolve failed connection");
                                        authInProgress = true;
                                        result.startResolutionForResult(MainActivity.this,
                                                AUTH_REQUEST);
                                    } catch (IntentSender.SendIntentException e) {
                                        Log.e(TAG,
                                                "Exception while starting resolution activity", e);
                                        MainActivity.this.finish();
                                    }
                                }
                                else {
                                    MainActivity.this.finish();
                                }
                            }
                        }
                )
                .build();
    }

    /**
     * Checks if there is any missing permissions, and
     * requests runtime permission if needed.
     */
    private void checkAndRequestPermissions() {
        // Check for permissions
        for (String eachPermission : REQUIRED_PERMISSION_LIST) {
            if (ContextCompat.checkSelfPermission(this, eachPermission) != PackageManager.PERMISSION_GRANTED) {
                missingPermission.add(eachPermission);
            }
        }
        // Request for missing permissions
        if (missingPermission.isEmpty()) {
            if (MainActivity.this.googleApiClient != null)
                MainActivity.this.googleApiClient.connect();
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            ActivityCompat.requestPermissions(this,
                    missingPermission.toArray(new String[missingPermission.size()]),
                    REQUEST_PERMISSION_CODE);
        } else {
            if (MainActivity.this.googleApiClient != null)
                MainActivity.this.googleApiClient.connect();
        }

    }

    /**
     * Result of runtime permission request
     */
    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        // Check for granted permission and remove from missing list
        if (requestCode == REQUEST_PERMISSION_CODE) {
            for (int i = grantResults.length - 1; i >= 0; i--) {
                if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                    missingPermission.remove(permissions[i]);
                }
            }
        }
        // If there is enough permission, we will start the registration
        if (missingPermission.isEmpty()) {
            initGoogleApiClient();
            if (MainActivity.this.googleApiClient != null)
                MainActivity.this.googleApiClient.connect();
        } else {
            Toast.makeText(getApplicationContext(), "Failed get permissions", Toast.LENGTH_LONG).show();
            finish();
        }
    }

    private void findDataSources() {
        Fitness.SensorsApi.findDataSources(googleApiClient, new DataSourcesRequest.Builder()
                .setDataTypes(DataType.TYPE_HEART_RATE_BPM)
                // .setDataTypes(DataType.TYPE_SPEED)
                // .setDataTypes(DataType.TYPE_STEP_COUNT_CUMULATIVE)
                .setDataSourceTypes(DataSource.TYPE_RAW)
                .build())
                .setResultCallback(new ResultCallback<DataSourcesResult>() {
                    @Override
                    public void onResult(DataSourcesResult dataSourcesResult) {

                        for (DataSource dataSource : dataSourcesResult.getDataSources()) {

                            if (dataSource.getDataType().equals(DataType.TYPE_HEART_RATE_BPM)
                                    && onDataPointListener == null) {
                                Log.d(TAG, "findDataSources onResult() registering dataSource=" + dataSource);
                                registerDataSourceListener(DataType.TYPE_HEART_RATE_BPM);

                            }
                        }
                    }
                });

    }


    private void registerDataSourceListener(DataType dataType) {
        onDataPointListener = new OnDataPointListener() {
            // 심박수가 측정되면 심박수를 얻어올 수 있는 콜백입니다
            @Override
            public void onDataPoint(DataPoint dataPoint) {
                for (Field field : dataPoint.getDataType().getFields()) {
                    Value aValue = dataPoint.getValue(field);
                    //Log.d(TAG, "Detected DataPoint field: " + field.getName());
                    //Log.d(TAG, "Detected DataPoint value: " + aValue);

                    //addContentToView("dataPoint=" + field.getName() + " " + aValue + "\n");
                    addContentToView(aValue.asFloat());
                }
            }
        };

        Fitness.SensorsApi.add(
                googleApiClient,
                new SensorRequest.Builder()
                        .setDataType(dataType)
                        .setSamplingRate(2, TimeUnit.SECONDS)
                        .setAccuracyMode(SensorRequest.ACCURACY_MODE_DEFAULT)
                        .build(),
                onDataPointListener)
                .setResultCallback(new ResultCallback<Status>() {
                    @Override
                    public void onResult(Status status) {
                        if (status.isSuccess()) {
                            Log.d(TAG, "onDataPointListener  registered good");
                        } else {
                            Log.d(TAG, "onDataPointListener failed to register bad");
                        }
                    }
                });

    }

    private void unregisterFitnessDataListener() {
        if (this.onDataPointListener == null) {
            return;
        }

        if (this.googleApiClient == null) {
            return;
        }

        if (this.googleApiClient.isConnected() == false) {
            return;
        }

        Fitness.SensorsApi.remove(
                this.googleApiClient,
                this.onDataPointListener)
                .setResultCallback(new ResultCallback<Status>() {
                    @Override
                    public void onResult(Status status) {
                        if (status.isSuccess()) {
                            Log.d(TAG, "Listener was removed!");
                        } else {
                            Log.d(TAG, "Listener was not removed.");
                        }
                    }
                });
        // [END unregister_data_listener]
    }


    @Override
    protected void onResume() {
        super.onResume();

        Log.d(TAG, "onStart connect attempted");
    }

    @Override
    protected void onStop() {
        super.onStop();
        unregisterFitnessDataListener();

        if (this.googleApiClient != null && this.googleApiClient.isConnected()) {
            this.googleApiClient.disconnect();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == AUTH_REQUEST) {
            authInProgress = false;

            if (resultCode == RESULT_OK) {

                if (!this.googleApiClient.isConnecting() && !this.googleApiClient.isConnected()) {
                    this.googleApiClient.connect();
                    Log.d(TAG, "onActivityResult googleApiClient.connect() attempted in background");

                }
            }
        }
    }

    private synchronized void addContentToView(final float value) {


        runOnUiThread(new Runnable() {

            @Override
            public void run() {

                if (spinner.getVisibility() == View.VISIBLE)
                    spinner.setVisibility(View.INVISIBLE);

                Log.d(TAG,"Heart Beat Rate Value : " + value);
                textMon.setText("Heart Beat Rate Value : " + value);
            }
        });
    }
}



7. Layout 파일입니다.

<!--
Copyright 2014 Google, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_activity_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="10dp"
    android:paddingTop="10dp"
    android:paddingRight="10dp"
    android:paddingBottom="10dp"
    android:background="#fff"
    android:weightSum="5"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2"
        android:orientation="vertical">

        <ProgressBar
            android:layout_gravity="center_horizontal"
            android:id="@+id/progressBar1"
            style="?android:attr/progressBarStyleLarge"
            android:layout_width="30dp"
            android:layout_weight="1"
            android:layout_height="30dp"
            android:layout_centerHorizontal="true" />

        <TextView
            android:id="@+id/textMon"
            android:textAlignment="center"
            android:layout_weight="1"
            android:layout_width="match_parent"
            android:layout_height="30dp" />

        <Button
            android:id="@+id/btnStart"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_margin="10dp"
            android:layout_weight="1"
            android:padding="3dp"
            android:text="START"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:textColor="#333"
            android:typeface="normal" />
    </LinearLayout>
</LinearLayout>

건투를 빕니다^^





번호 제목 글쓴이 날짜 조회 수
공지 [TIP] 안드로이드 앱 빌드시 "Error:Execution failed for task ':app:compileDebugJavaWithJavac'" 오류가 발생할 경우 파이팅건맨 2017.09.13 7219
공지 [TIP] 죽지 않는 안드로이드 서비스 만들기 (Unstoppable service) [6] 파이팅건맨 2015.06.26 6642
공지 [TIP] 안드로이드 앱이 처음 설치될때 Referrer 정보 받아 오기 파이팅건맨 2016.08.30 3725
44 tizen .net wearable widget 질문해도 될까요? [1] 갤럭시규 2019.08.19 21
43 [TIP] iOS - UIWebView에 로컬 html 파일 로드하기 (Swift 4) 파이팅건맨 2019.06.05 77
42 [TIP] Tizen Push가 갑자기 내려오지 않을때 파이팅건맨 2019.05.20 43
41 [TIP] 두근두근앱이 라즈베리파이를 두근거리도록 개발한 기록 #3 파이팅건맨 2019.05.15 176
40 [TIP] 두근두근앱이 라즈베리파이를 두근거리도록 개발한 기록 #2 파이팅건맨 2019.05.11 184
39 [TIP] Android 코드에서 블루투스 연결이 잘 안될때 파이팅건맨 2019.05.11 266
38 [TIP] 두근두근앱이 라즈베리파이를 두근거리도록 개발한 기록 #1 파이팅건맨 2019.05.07 273
37 [TIP] 타이젠 스튜디오에서 웨어러블 디바이스로 디버깅을 위한 바이너리 전송이 안 될때 파이팅건맨 2019.03.26 176
36 [TIP] http://tizen.org/system/tizenid 으로 타이젠 고유 id를 확보할 때 유의할 점 파이팅건맨 2019.03.14 83
35 [TIP] "cordova run android" 명령을 실행했는데 "A problem occurred evaluating project ':CordovaLib'"오류가 뜰때 파이팅건맨 2019.01.22 331
34 [TIP] Mac에서 Cordova run android 를 실행했는데 "Command failed with exit code EACCES" 오류가 뜰때 파이팅건맨 2019.01.22 159
» [TIP] 안드로이드에서 심박수 측정하는 코드 [6] 파이팅건맨 2019.01.02 651
32 [TIP] Google Cloud API 사용시 안드로이드의 Assets 폴더에 있는 Crendential 파일 사용하기 파이팅건맨 2018.05.29 238
31 [TIP] 안드로이드 스튜디오에서 Error:android-apt plugin is incompatible with the Android Gradle plugin. Please use 'annotationProcessor' configuration instead 오류날때 파이팅건맨 2018.02.02 2058
30 [TIP] 안드로이드 앱 빌드시 "Error:Execution failed for task ':app:compileDebugJavaWithJavac'" 오류가 발생할 경우 파이팅건맨 2017.09.13 7219
29 [TIP] Error:java.lang.OutOfMemoryError: GC overhead limit exceeded 파이팅건맨 2016.11.03 889
28 [TIP] 안드로이드 앱이 처음 설치될때 Referrer 정보 받아 오기 파이팅건맨 2016.08.30 3725
27 [TIP] 안드로이드에서 구글 스프레드시트에 데이터 쓰기 파이팅건맨 2016.05.04 2496
26 [TIP] 안드로이드에서 키보드가 나타날 때 레이아웃이 위로 움직인다면 파이팅건맨 2016.03.22 852
25 [TIP] 안드로이드 - 설치된 앱 목록 얻기 파이팅건맨 2016.03.22 3166
위로