심박수를 측정할 수 있는 센서가 부착된 안드로이드 스마트폰에서 심박수를 측정하는 코드 예제 입니다.
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>
** 추가합니다 **
build.gradle 파일의 모습입니다.
apply plugin: 'com.android.application' android { compileSdkVersion 28 buildToolsVersion '28.0.3' defaultConfig { applicationId "org.airpage.heartbeat" minSdkVersion 21 targetSdkVersion 28 versionCode 1 versionName '1.0.0' } buildTypes { release { minifyEnabled true } debug { minifyEnabled false } } repositories { maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } } lintOptions { checkReleaseBuilds false // Or, if you prefer, you can continue to check for errors in release builds, // but continue the build even when errors are found: abortOnError false } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support:appcompat-v7:28.+' implementation 'com.android.support:customtabs:28.+' implementation 'com.android.support:support-v4:28.+' implementation 'com.android.support.constraint:constraint-layout:1.1.2' implementation 'com.google.android.gms:play-services-fitness:16.0.1' implementation 'com.google.android.gms:play-services-auth:16.0.1' } apply plugin: 'com.google.gms.google-services'
건투를 빕니다^^
이 게시물이 | |
AiRPAGE가 |
댓글 18
-
miri
2019.05.15 19:30
-
안녕하세요? 90번째 라인에서 버튼을 비활성화 해두었습니다. 그리고 심박측정에 관한 권한을 얻으면 자동으로 해당 버튼을 활성시키도록 141번째 라인에 코드가 있습니다.
아마 권한 획득 진행이 잘 안되고 있는 것 같습니다. 디버그 키를 정확하게 등록하셨는지 확인해 보셔야 할 것 같습니다.
건투를 빕니다^^
-
코드!
2019.05.29 13:46
디버그키를 정확히 어떻게 얻어서 어디에 등록을 해야되는지 궁금합니다.
코드에 오류는 없는데 실행 자체가 안되네요 ㅠㅠ
-
본문글의 2~4번 항목에 말씀하신 질문의 답이 있습니다. 만약 mac 환경이 아니라 windows환경에서 작업중이시라면, 아래 경로에서 디버그 키를 확인하실 수 있을 겁니다.
C:\Users\<USERNAME>\.android\debug.keystore
<USERNAME> 부분을 코드!님의 환경에 맞게 수정해서 사용하시길...
건투를 빕니다^^
-
코드
2019.05.30 20:22
감사합니다. 실행완료했습니다!
혹시 실례지만 코드에 대한 간단한 주석처리나
설명을 받을 수 있는 방법이 있을까요?!
-
축하드립니다!
주석은... 틈나는 대로 조심씩 달아 놓도록 하겠습니다^^ -
잘보고있어요!
2019.10.11 15:55
선생님 안녕하세요!
import이런거 제 안드로이드 스튜디오 버전에 맞게 다시 고치고
몇몇개 오류는 없어지는데 너무 많은 오류가 있어서요...ㅠㅠ
제가 과제에 이 코드를 활용한 앱을 만드려고 하는데
혹시 시간괜찮으시다면 원격으로 뭐가 오류인지 봐주실수있나요...?
너무 간절합니다ㅠㅠ
-
아... 이곳을 통해 문의해 주시는 내용에만 (최대한)답을 해드리려고 노력하고 있습니다. 이곳 외의 채널을 통하는 것은 곤란합니다. 죄송합니다.
-
ㅇㅇ
2019.10.13 12:10
안드로이드 스튜디오 좌측 라이브러리가 어디에있나요?
-
'안드로이드 스튜디오'가 아닌 '구글 APIs' 콘솔에서 해당 메뉴를 찾으셔야 합니다. 아래링크를 참고하세요.
https://console.developers.google.com건투를 빕니다!
-
도와주세요
2019.10.13 14:09
선생님 통화로 오류좀 알려주시면 안되나요 오류가 너무 많아요.... 부탁드립니다.
-
곤란합니다. 이곳을 최대한 활용 부탁드리겠습니다.
-
아라리
2019.10.13 14:13
import com.google.android.gms.common.ConnectionResult;
이 부분에서 .android.부분이 빨간색으로 나오고 can not resolve symbol 'android'라고 뜨면서 import문이 오류가 나서 아래에 오류가 많은거 같은데 왜 이럴까요?
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.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.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; -
build.gradle 의 모습을 상기의 글에 추가하였습니다. 참고 부탁드립니다.
즉, 아래 내용을 build.gradle에 추가해 주세요,
dependencies { : implementation 'com.google.android.gms:play-services-fitness:16.0.1' implementation 'com.google.android.gms:play-services-auth:16.0.1' : } // 파일의 가장 아래 apply plugin: 'com.google.gms.google-services'
건투를 빕니다!
-
새봄
2019.10.13 16:57
안녕하세용!!
혹시 메인액티비티에 버튼을 만들고
그 버튼을 누를시 다른 액티비티에 저 심박도 측정화면을 블러오고 싶은데요
인텐트 연결을 해서 please wait.... 까지는 뜨는데 여기서 start버튼으로 넘어가지가 않네요..ㅠㅠ
방법이 없을까요..?
-
상기 코드에서 보시면 아시겠지만, "please wait"메시지가 "Start"로 바뀌는 조건은-initGoogleApiClient() 가 잘 호출이 되어서 구글 API 클라이언트로 로그인이 완료되고 권한도 다 얻었을 때 입니다.즉,GoogleApiClient.ConnectionCallbacks() {//Google API 클라이언트의 로그인에 성공하면 호출이 되는 콜백입니다@Overridepublic void onConnected(Bundle bundle) {의 onConnected 콜백이 정상으로 불려 졌을 때 입니다.구글 계정과 연결이 잘 안되었다면 아래 경로에서 구글 API 클라이언트가 잘 활성화 되었는지,프로젝트를 잘 연결하였는지를 확인하셔야 할 것 같습니다.onConnectionSuspended,onConnectionFailed콜백이 호출 되는지를 확인하시고, 호출이 된다면 오류 메시지를 확인한 후 적절한 대응을 하셔야 할 것 같습니다.
-
ㅇㅇ
2019.12.23 18:30
안녕하세요!
메인액티비티에서 버튼을 눌러서 위 코드를 실행시키려고 하는데요.
심박수센서에 손을 대고 있으면 어쩔때는 심박수가 잘 나오고 어떨 때는 인식이 안되는 데 이것은 센서 문제인건가요..?
그리고 심박수를 받아서 띄우는 것까지 시간간격을 조절할 수 있나요?
그리고 구글 지도 APIKEY는 메니페스트 파일에 아래와 같이 기입하는데 Fitness API 는 메니페스트 파일에 APIKEY를 적어주지 않아도 괜찮나요..?
혹시 맞다면 android:name 부분을 어떻게 적어야하는건지 알려주실 수 있으신가요..?
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="APIKEY~~"/> -
안녕하세요?
첫번째 심박 데이터가 들어오기까지 시간이 걸리기도 하고 또 빨리 처리 되기도 하는 것 같습니다. 정확한 이유는 저도 잘 모르겠습니다^^
심박수를 측정하는 간격은 "setSamplingRate" 함수의 첫번째, 두번째 파라메터 값을 수정하시면 될 것 같습니다.그리고 Fitness API 는 코드에 API 키를 넣을 필요는 없습니다. (상기에 써 놓은 것 처럼) 대신 Google Developer Console 에서 앱의 인증서 지문 값만 등록하면 됩니다.
심박 데이터가 표시된다고 하셨으니 관련 설정은 다 잘 하신 것 같습니다.
건투를 빕니다, 메리 크리스마스!^^
안녕하세요 지금 심박수 센서 공부중인 대학생입니다.
설정도 다 똑같이 하고 안드로이드 스튜디오에서 오류도 없는데 왜 실행시키면 wait please 버튼이 눌리지 않을까요..?