728x90

실제 개발 테스트할 앱을 신규로 추가하는 과정에서 SHA-1 키를 등록할 수 있다.


기존 생성된 프로젝트에서 모듈을 추가하는 방식으로 com.link2me.android.gmap 으로 프로젝트를 생성하는 과정이다.









패키지 이름에 생성한 앱의 패키지명을 입력하고, 위에서 획득한 SHA1 인증서 지문을 넣고 저장 버튼을 누르면 구글 Map API 사용을 위한 설정준비가 완료된다.



블로그 이미지

Link2Me

,
728x90

회사 강의자료 만들기 위한 목적으로 작성한 게시글이라 구글 클라우드 플랫폼을 한번도 사용 안해본 계정을 이용하여 테스트한 걸 적는다.

 

구글 클라우드 플랫폼 : https://cloud.google.com

 

아래 이미지 번호 순서대로 진행하면 된다.

구글이 계속 화면 배치를 변경하기 때문에 아래 그림 순서와 다를 수 있다.

 

 

 

API 및 서비스를 선택한다.

 

대시보드를 선택하면 프로젝트가 없기 때문에 프로젝트를 만들기를 해야 한다.

 

프로젝트 이름은 개발자 본인이 원하는 이름으로 지정한다.

 

 

상단 검색 기능을 이용해서 찾아도 되고, Map API를 이용하기 위해서 추천으로 나온 9번을 누른다.

 

 

사용자 인증정보를 만든다.

 

[동의화면 구성]이 새롭게 추가된 기능인 거 같다.

 

User Type 내부 선택은 아예 안된다. 13번, 14번을 한다.

 

요청하는 정보를 입력한다.

 

 

사용자 인증 정보 만들기를 누르면 팝업창이 나온다.

 

API 키를 선택하면 자동으로 API 를 생성해준다.

 

 

 

 

Android 앱 항목추가를 안하고 저장을 누르면 아래와 같이 제한사항 "없음"으로 표시된다.

21번 수정아이콘을 눌러서 등록을 해준다.

 

 

SHA-1 인증서 지문 등록하는 방법은 다음 게시글 https://link2me.tistory.com/1915 참조하면 된다.

위 방법은 메뉴가 없어진 것 같아서 CMD 창에서 하는 방법으로 https://link2me.tistory.com/1700 게시글을 참조하면 된다.

 

 

이제 구글 Map API 사용을 위한 설정 준비가 완료된 것이다.

 

debug.keystore 경로를 인식하지 못할 경우에는 직접 해당 경로로 이동하여

keytool -list -v -alias androiddebugkey -keystore debug.keystore

를 입력하고 패스워드 android 를 입력하면 된다.

보통 Users 폴더 하단에서 검색하면 빠르게 찾아낼 수 있다.

블로그 이미지

Link2Me

,
728x90

https://link2me.tistory.com/1703 게시글에 작성된 것을 코틀린으로 변경한 것이다.


dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'com.google.android.material:material:1.1.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.google.android.gms:play-services-maps:17.0.0' // 구글지도 라이브러리
    implementation 'com.google.android.gms:play-services-location:17.0.0' // 위치정보 라이브러리
}

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.SEND_SMS" />


activity_current_place.xml

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:map="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/realtimemap"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MapsActivity" />


Android Studio 에서 제공하는 Java파일을 코틀린 파일로 변환하는 기능을 이용해서 변환하고 일부 코드를 좀 수정했다.

package com.link2me.android.googlemap

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.pm.PackageManager
import android.location.Address
import android.location.Geocoder
import android.location.Location
import android.os.Bundle
import android.os.Looper
import android.telephony.SmsManager
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.gms.location.*
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.*
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*

class CurrentPlace : AppCompatActivity(), OnMapReadyCallback {
    private val TAG = this.javaClass.simpleName
    private lateinit var mContext: Context
    private lateinit var mMap: GoogleMap
    private var currentMarker: Marker? = null

    private lateinit var mFusedLocationProviderClient : FusedLocationProviderClient
    private lateinit var locationRequest: LocationRequest
    private var mCurrentLocatiion: Location? = null
    private var mCameraPosition: CameraPosition? = null
    private val mDefaultLocation = LatLng(37.56, 126.97)
    private var mLocationPermissionGranted = false
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        savedInstanceState?.let{
            mCurrentLocatiion = it.getParcelable(KEY_LOCATION)
            mCameraPosition = it.getParcelable(KEY_CAMERA_POSITION)
        }
        setContentView(R.layout.activity_current_place)
        mContext = this@CurrentPlace

        locationRequest = LocationRequest()
            .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) // 정확도를 최우선적으로 고려
            .setInterval(UPDATE_INTERVAL_MS.toLong()) // 위치가 Update 되는 주기
            .setFastestInterval(FASTEST_UPDATE_INTERVAL_MS.toLong()) // 위치 획득후 업데이트되는 주기
        val builder = LocationSettingsRequest.Builder()
        builder.addLocationRequest(locationRequest)

        // Construct a FusedLocationProviderClient.
        mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)

        // Build the map.
        val mapFragment = supportFragmentManager.findFragmentById(R.id.realtimemap) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }

    /**
     * Saves the state of the map when the activity is paused.
     */
    override fun onSaveInstanceState(outState: Bundle) {
        mMap?.let{
            outState.putParcelable(KEY_CAMERA_POSITION, it.cameraPosition)
            outState.putParcelable(KEY_LOCATION, mCurrentLocatiion)
            super.onSaveInstanceState(outState)
        }
    }

    override fun onMapReady(map: GoogleMap) {
        Log.e(TAG, "onMapReady :")
        mMap = map
        setDefaultLocation() // GPS를 찾지 못하는 장소에 있을 경우 지도의 초기 위치가 필요함.
        locationPermission
        updateLocationUI()
        deviceLocation
    }

    private fun updateLocationUI() {
        mMap?.let{
            try {
                if (mLocationPermissionGranted) {
                    it.isMyLocationEnabled = true
                    it.uiSettings.isMyLocationButtonEnabled = true
                } else {
                    it.isMyLocationEnabled = false
                    it.uiSettings.isMyLocationButtonEnabled = false
                    mCurrentLocatiion = null
                    locationPermission
                }
            } catch (e: SecurityException) {
                Log.e("Exception: %s", e.message!!)
            }
        }
    }

    private fun setDefaultLocation() {
        currentMarker?.let{
            it.remove()
        }
        val markerOptions = MarkerOptions()
        markerOptions.position(mDefaultLocation)
        markerOptions.title("위치정보 가져올 수 없음")
        markerOptions.snippet("위치 퍼미션과 GPS 활성 여부 확인하세요")
        markerOptions.draggable(true)
        markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED))
        currentMarker = mMap!!.addMarker(markerOptions)
        val cameraUpdate = CameraUpdateFactory.newLatLngZoom(mDefaultLocation, 15f)
        mMap.moveCamera(cameraUpdate)
    }

    fun getCurrentAddress(latlng: LatLng): String {
        // 위치 정보와 지역으로부터 주소 문자열을 구한다.
        var addressList: List<Address>? = null
        val geocoder =
            Geocoder(this, Locale.getDefault())

        // 지오코더를 이용하여 주소 리스트를 구한다.
        addressList = try {
            geocoder.getFromLocation(latlng.latitude, latlng.longitude, 1)
        } catch (e: IOException) {
            Toast.makeText(
                this,
                "위치로부터 주소를 인식할 수 없습니다. 네트워크가 연결되어 있는지 확인해 주세요.",
                Toast.LENGTH_SHORT
            ).show()
            e.printStackTrace()
            return "주소 인식 불가"
        }
        if (addressList.size < 1) { // 주소 리스트가 비어있는지 비어 있으면
            return "해당 위치에 주소 없음"
        }

        // 주소를 담는 문자열을 생성하고 리턴
        val address = addressList[0]
        val addressStringBuilder = StringBuilder()
        for (i in 0..address.maxAddressLineIndex) {
            addressStringBuilder.append(address.getAddressLine(i))
            if (i < address.maxAddressLineIndex) addressStringBuilder.append("\n")
        }
        return addressStringBuilder.toString()
    }

    var locationCallback: LocationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult) {
            super.onLocationResult(locationResult)
            val locationList =
                locationResult.locations
            if (locationList.size > 0) {
                val location = locationList[locationList.size - 1]
                val currentPosition =
                    LatLng(location.latitude, location.longitude)
                val markerTitle = getCurrentAddress(currentPosition)
                val markerSnippet =
                    "위도:" + location.latitude.toString() + " 경도:" + location.longitude
                        .toString()
                Log.d(TAG,"Time :" + CurrentTime() + " onLocationResult : " + markerSnippet)

                //현재 위치에 마커 생성하고 이동
                setCurrentLocation(location, markerTitle, markerSnippet)
                mCurrentLocatiion = location
            }
        }
    }

    private fun CurrentTime(): String {
        val today = Date()
        val date = SimpleDateFormat("yyyy/MM/dd")
        val time = SimpleDateFormat("hh:mm:ss a")
        return time.format(today)
    }

    fun setCurrentLocation(
        location: Location,
        markerTitle: String?,
        markerSnippet: String?
    ) {
        if (currentMarker != null) currentMarker!!.remove()
        val currentLatLng = LatLng(location.latitude, location.longitude)
        val markerOptions = MarkerOptions()
        markerOptions.position(currentLatLng)
        markerOptions.title(markerTitle)
        markerOptions.snippet(markerSnippet)
        markerOptions.draggable(true)
        currentMarker = mMap!!.addMarker(markerOptions)
        mMap.setOnMarkerClickListener {
            val inflater =
                mContext!!.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            val layout = inflater.inflate(
                R.layout.send_message_popup,
                null
            )
            val lms_confirm =
                AlertDialog.Builder(mContext!!)
            lms_confirm.setTitle("발송할 휴대폰번호 등록")
            lms_confirm.setView(layout)
            val etphoneNO =
                layout.findViewById<View>(R.id.phoneno) as EditText
            // 확인 버튼 설정
            lms_confirm.setPositiveButton(
                "등록",
                DialogInterface.OnClickListener { dialog, which ->
                    val phoneNO =
                        etphoneNO.text.toString().trim { it <= ' ' }
                    if (phoneNO.length == 0) {
                        val phoneNO_confirm =
                            AlertDialog.Builder(mContext!!)
                        phoneNO_confirm.setMessage("휴대폰 번호를 입력하세요.").setCancelable(false)
                            .setPositiveButton(
                                "확인"
                            ) { dialog, which -> // 'YES'
                                dialog.dismiss()
                            }
                        val alert =
                            phoneNO_confirm.create()
                        alert.show()
                        return@OnClickListener
                    }
                    val gps_location =
                        location.latitude.toString() + "," + location.longitude
                            .toString()
                    SMS_Send(phoneNO, gps_location)
                })
            lms_confirm.setNegativeButton(
                "취소"
            ) { dialog, which -> dialog.dismiss() }
            lms_confirm.show()
            true
        }
        val cameraUpdate = CameraUpdateFactory.newLatLng(currentLatLng)
        mMap.moveCamera(cameraUpdate)
    }

    private fun SMS_Send(phoneNO: String, message: String ) { // SmsManager API
        var sms_message = "구글 지도 위치를 보내왔습니다.\n"
        sms_message += "http://maps.google.com/maps?f=q&q=$message\n누르면 상대방의 위치를 확인할 수 있습니다."
        Log.e(TAG, "SMS : $sms_message")
        try {
            //전송
            val smsManager = SmsManager.getDefault()
            val parts = smsManager.divideMessage(sms_message)
            smsManager.sendMultipartTextMessage(phoneNO, null, parts, null, null)
            Toast.makeText(
                applicationContext,
                "위치전송 문자보내기 완료!",
                Toast.LENGTH_LONG
            ).show()
        } catch (e: Exception) {
            Toast.makeText(
                applicationContext,
                "SMS faild, please try again later!",
                Toast.LENGTH_LONG
            ).show()
            e.printStackTrace()
        }
    }

    private val deviceLocation: Unit
        private get() {
            try {
                if (mLocationPermissionGranted) {
                    mFusedLocationProviderClient!!.requestLocationUpdates(
                        locationRequest,
                        locationCallback,
                        Looper.myLooper()
                    )
                }
            } catch (e: SecurityException) {
                Log.e("Exception: %s", e.message!!)
            }
        }

    private val locationPermission: Unit
        private get() {
            if (ContextCompat.checkSelfPermission(this.applicationContext,
                    Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
            ) {
                mLocationPermissionGranted = true
            } else {
                ActivityCompat.requestPermissions(
                    this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                    PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION
                )
            }
        }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        mLocationPermissionGranted = false
        when (requestCode) {
            PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION -> {
                if (grantResults.size > 0
                    && grantResults[0] == PackageManager.PERMISSION_GRANTED
                ) {
                    mLocationPermissionGranted = true
                }
            }
        }
        updateLocationUI()
    }

    @SuppressLint("MissingPermission")
    override fun onStart() {
        super.onStart()
        if (mLocationPermissionGranted) {
            Log.d(TAG, "onStart : requestLocationUpdates")
            mFusedLocationProviderClient!!.requestLocationUpdates(
                locationRequest,
                locationCallback,
                null
            )
            mMap?.let{
                it.isMyLocationEnabled = true
            }
        }
    }

    override fun onStop() {
        super.onStop()
        mFusedLocationProviderClient?.let{
            it!!.removeLocationUpdates(locationCallback)
            Log.d(TAG, "onStop : removeLocationUpdates")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        mFusedLocationProviderClient?.let{
            it.removeLocationUpdates(locationCallback)
            Log.d(TAG, "onDestroy : removeLocationUpdates")
        }
    }

    companion object {
        private const val DEFAULT_ZOOM = 15
        private const val PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1
        private const val GPS_ENABLE_REQUEST_CODE = 2001
        private const val UPDATE_INTERVAL_MS = 1000 * 60 * 15 // 1분 단위 시간 갱신
        private const val FASTEST_UPDATE_INTERVAL_MS = 1000 * 30 // 30초 단위로 화면 갱신
        private const val KEY_CAMERA_POSITION = "camera_position"
        private const val KEY_LOCATION = "location"
    }
}




블로그 이미지

Link2Me

,
728x90

구글에서 제공하는 함수를 이용하여 거리를 계산하는 걸 시도했으나 실패를 했다.

API_KEY를 새로 생성해서도 해보고 기존 API를 이용해서도 해보았으나, 역시 잘 안된다.




위와 같이 설정하면 얼추 동작이 되는 거 같기도 하다.



private void getMapDistanceData(String latitude, String longitude) {
    // 1. RequestQueue 생성 및 초기화
    RequestQueue requestQueue = Volley.newRequestQueue(mContext);
    String url = "https://maps.googleapis.com/maps/api/distancematrix/json?units=metric&mode=transit&origins=37.541,126.986&destinations=35.1595454,126.8526012&region=KR&key=API_KEY";

    // 2. Request Obejct인 StringRequest 생성
    StringRequest request = new StringRequest(Request.Method.POST, url,
            new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    Log.d("result", "[" + response + "]");
                    showJSONList(response);
                }
            },
            new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Log.d("error", "[" + error.getMessage() + "]");
                }
            }) {
        @Override
        protected Map<String, String> getParams() throws AuthFailureError {
            Map<String, String> params = new HashMap<>();
            params.put("keyword", Value.encrypt(Value.URLkey()));
            params.put("reg_date", Value.encrypt(Value.Today()));
            return params;
        }
    };

    // 3) 생성한 StringRequest를 RequestQueue에 추가
    requestQueue.add(request);
}
 


두 좌표(위도, 경도)간의 거리결과를 얻는 것이 목적이라 위 코드는 포기하고 PHP 코드를 검색해서 만들어서 사용했더니 만족스런 결과가 나온다.

function getDistance($lat1, $lng1, $lat2, $lng2) { // 위, 경도 거리 계산
    $earth_radius = 6371;
    $dLat = deg2rad($lat2 - $lat1);
    $dLon = deg2rad($lng2 - $lng1);
    $a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon/2) * sin($dLon/2);
    $c = 2 * asin(sqrt($a));
    $d = $earth_radius * $c;
    return $d * 1000; // m 거리 반환
    //return $d; // km 거리 반환
}

function StDistance($StID, $lat1, $lng1){
    if(strlen($lat1) < 1 || strlen($lng1) < 1) {
        return "X";
    }
    $sql = "select latitude,longitude from Station where StID=?";
    $params = array($StID);
    $stmt = $this->db->prepare($sql);
    $stmt->execute($params);
    if($row = $stmt->fetch()){
        return $this->getDistance($lat1, $lng1, $row[0], $row[1]);
    } else {
        return "X";
    }
}


DB에 저장된 위도, 경도값과 현재 위치에서 반환한 위도, 경도 값의 차이를 기준으로 거리 계산을 하도록 하면 원하는 결과를 얻을 수 있다.


블로그 이미지

Link2Me

,
728x90

Firebase 에서 구글 인증으로 로그인 처리하는 방법에 대한 사항이다.


1. https://console.firebase.google.com/ 에 접속한다.

  

   프로젝트가 없으면 추가하고 있으면 선택한다.


2. 앱을 설정하는 일련의 과정은 그림으로 대신한다.


3. 앱 build.gradle 파일 내용

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"

    defaultConfig {
        applicationId "com.link2me.android.googleauth"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.firebase:firebase-analytics:17.2.0'
    implementation 'com.google.firebase:firebase-auth:19.1.0'  // Firebase 인증에 대한 항목을 추가
    implementation 'com.google.android.gms:play-services-auth:17.0.0'
}

apply plugin: 'com.google.gms.google-services'


4. XML Layout 작성

<RelativeLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center" >

    <!-- 구글 로그인 버튼 -->
    <com.google.android.gms.common.SignInButton
        android:id="@+id/sign_in_button"
        android:layout_width="254dp"
        android:layout_height="50sp">
    </com.google.android.gms.common.SignInButton>

    <ImageView
        android:id="@+id/sign_out_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/sign_in_button"
        android:background="@drawable/google_signout"/>

    <TextView
        android:id="@+id/mStatusTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/sign_in_button"
        android:layout_marginTop="99dp"
        android:text="TextView" />

</RelativeLayout >


5. 사용자 인증 코드 작성

https://firebase.google.com/docs/auth/android/google-signin?authuser=0 에서 필요한 코드를 복사하여 붙여넣기 한다.

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthCredential;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.GoogleAuthProvider;

public class GoogleSignInActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "GoogleActivity";
    private static final int RC_SIGN_IN = 9001;
    private FirebaseAuth mAuth;
    private GoogleSignInClient mGoogleSignInClient;

    private TextView mStatusTextView;

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

        // Button listeners
        findViewById(R.id.sign_in_button).setOnClickListener(this);
        findViewById(R.id.sign_out_button).setOnClickListener(this);
        mStatusTextView = findViewById(R.id.mStatusTextView);

        // Configure Google Sign In
        GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(getString(R.string.default_web_client_id))
                .requestEmail()
                .build();

        mGoogleSignInClient = GoogleSignIn.getClient(this, gso);

        // Initialize Firebase Auth
        mAuth = FirebaseAuth.getInstance();
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.sign_in_button:
                signIn(); // 구글 로그인 버튼 클릭시
                break;
            case R.id.sign_out_button:
                signOut();
                break;
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        // Check if user is signed in (non-null) and update UI accordingly.
        FirebaseUser currentUser = mAuth.getCurrentUser();
        updateUI(currentUser);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
        if (requestCode == RC_SIGN_IN) {
            Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
            try {
                // Google Sign In was successful, authenticate with Firebase
                GoogleSignInAccount account = task.getResult(ApiException.class);
                firebaseAuthWithGoogle(account);
            } catch (ApiException e) {
                Log.w(TAG, "Google sign in failed", e);
                updateUI(null);
            }
        }
    }

    private void firebaseAuthWithGoogle(GoogleSignInAccount acct) {
        Log.d(TAG, "firebaseAuthWithGoogle:" + acct.getId());

        AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
        mAuth.signInWithCredential(credential)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        if (task.isSuccessful()) {
                            // Sign in success, update UI with the signed-in user's information
                            Log.e(TAG, "signInWithCredential:success");
                            FirebaseUser user = mAuth.getCurrentUser();
                            updateUI(user);
                            Log.e(TAG,"user getEmail : "+user.getEmail());
                            Log.e(TAG,"user getDisplayName : "+user.getDisplayName());
                            Log.e(TAG,"user getUid : "+user.getUid());
                        } else {
                            // If sign in fails, display a message to the user.
                            Log.w(TAG, "signInWithCredential:failure", task.getException());
                            Toast.makeText(GoogleSignInActivity.this, "Authentication Failed.", Toast.LENGTH_SHORT).show();
                            updateUI(null);
                        }

                        // [START_EXCLUDE]
                        //hideProgressDialog();
                        // [END_EXCLUDE]
                    }
                });
    }

    private void signIn() {
        Intent signInIntent = mGoogleSignInClient.getSignInIntent();
        startActivityForResult(signInIntent, RC_SIGN_IN);
    }

    private void signOut() {
        mAuth.signOut(); // Firebase sign out

        // Google sign out
        mGoogleSignInClient.signOut().addOnCompleteListener(this,
                new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        updateUI(null);
                    }
                });
    }

    private void revokeAccess() {
        // Firebase sign out
        mAuth.signOut();

        // Google revoke access
        mGoogleSignInClient.revokeAccess().addOnCompleteListener(this,
                new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        updateUI(null);
                    }
                });
    }

    private void updateUI(FirebaseUser user) {
        //hideProgressDialog();
        if (user != null) {
            mStatusTextView.setText(user.getEmail() + " : " + user.getDisplayName());

            findViewById(R.id.sign_in_button).setVisibility(View.GONE);
            findViewById(R.id.sign_out_button).setVisibility(View.VISIBLE);
        } else {
            mStatusTextView.setText("LogOut");

            findViewById(R.id.sign_in_button).setVisibility(View.VISIBLE);
            findViewById(R.id.sign_out_button).setVisibility(View.GONE);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //FirebaseAuth.getInstance().signOut();
    }
}


6. 로그인 후 Firebase 에 회원정보 저장 결과





블로그 이미지

Link2Me

,
728x90

현재 위치를 문자로 보내기

현재 내 위치를 문자로 상대방에게 보내는 기능을 구현 테스트 해봤다.

2010년대 초반대에 지도가 막 대세로 떠오르던 그 시절에 서비스 기획하면서 내 위치를 상대방에게 보내주면 주변에서 내가 헤메고 있을 때 유용할 거 같았다.

개인정보 등에 민감한 사항이지만 내 위치를 보내주는 것은 괜찮지 않을까 하는 생각이 들었다.

어디까지나 기능 구현 차원에서 접근하는 것이니 이점은 감안하고 참고하시길 ^^


AndroidManifest.xml

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- GPS -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.SEND_SMS" />



send_message_popup.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <EditText
        android:id="@+id/phoneno"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:singleLine="true"
        android:textSize="16sp"
        android:textColor="#000000" />

</LinearLayout>


https://link2me.tistory.com/1703에서  setCurrentLocation 메소드 부분을 아래와 같이 수정한다.


public void setCurrentLocation(final Location location, String markerTitle, String markerSnippet) {
    if (currentMarker != null) currentMarker.remove();

    LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude());

    MarkerOptions markerOptions = new MarkerOptions();
    markerOptions.position(currentLatLng);
    markerOptions.title(markerTitle);
    markerOptions.snippet(markerSnippet);
    markerOptions.draggable(true);

    currentMarker = mMap.addMarker(markerOptions);
    mMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
        @Override
        public boolean onMarkerClick(Marker marker) {
            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(LAYOUT_INFLATER_SERVICE);
            final View layout = inflater.inflate(R.layout.send_message_popup,null);
            AlertDialog.Builder lms_confirm = new AlertDialog.Builder(mContext);
            lms_confirm.setTitle("발송할 휴대폰번호 등록");
            lms_confirm.setView(layout);
            final EditText etphoneNO = (EditText) layout.findViewById(R.id.phoneno);
            // 확인 버튼 설정
            lms_confirm.setPositiveButton("등록", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    String phoneNO = etphoneNO.getText().toString().trim();
                    if(phoneNO.length() == 0) {
                        AlertDialog.Builder phoneNO_confirm = new AlertDialog.Builder(mContext);
                        phoneNO_confirm.setMessage("휴대폰 번호를 입력하세요.").setCancelable(false).setPositiveButton("확인",
                                new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) { // 'YES'
                                        dialog.dismiss();
                                    }
                                });
                        AlertDialog alert = phoneNO_confirm.create();
                        alert.show();
                        return;
                    }
                    String gps_location =String.valueOf(location.getLatitude())+","+String.valueOf(location.getLongitude());
                    SMS_Send(phoneNO,gps_location );
                }
            });
            lms_confirm.setNegativeButton("취소", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            });
            lms_confirm.show();
            return true;
        }
    });

    CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLng(currentLatLng);
    mMap.moveCamera(cameraUpdate);
}

private void SMS_Send(String phoneNO, String message){ // SmsManager API
    String sms_message = "구글 지도 위치를 보내왔습니다.\n";
    sms_message += "http://maps.google.com/maps?f=q&q="+message+"\n"+"누르면 상대방의 위치를 확인할 수 있습니다.";
    Log.e(TAG,"SMS : "+sms_message);
    try {
        //전송
        SmsManager smsManager = SmsManager.getDefault();
        ArrayList<String> parts = smsManager.divideMessage(sms_message);
        smsManager.sendMultipartTextMessage(phoneNO, null, parts, null, null);
        Toast.makeText(getApplicationContext(), "위치전송 문자보내기 완료!", Toast.LENGTH_LONG).show();
    } catch (Exception e) {
        Toast.makeText(getApplicationContext(), "SMS faild, please try again later!", Toast.LENGTH_LONG).show();
        e.printStackTrace();
    }
}


2019.12.15 일 추가 사항

SMS 보내기가 안된다는 댓글 문의가 있어서 적는다.

위험권한에 해당하는 기능은 별도 처리를 해줘야 한다.

https://www.tutorialspoint.com/android/android_sending_sms.htm 에 나온 내용을 참조하거나, https://link2me.tistory.com/1532 에 설명된 TED 퍼미션 사용법을 이용하면 된다.


도움이 되셨다면 ... 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.

블로그 이미지

Link2Me

,
728x90

구글 제공 구글맵 샘플코드는 https://github.com/googlemaps/android-samples/tree/master 에서 다운로드 받아서 필요한 것을 구현하면 된다.


구글 맵에 현재 위치를 표시하는 걸 Activity 위헤서 동작하는 걸 구현 테스트했다면, 이제 Fragment 상에서 동작되도록 하는 법을 기술한다.


<!-- Activity에서는 fragment로 구현해야 되지만 Fragment로 구현하기위해서는 MapView를 주로 사용-->
<com.google.android.gms.maps.MapView
    android:id="@+id/map"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.google.android.gms.maps.MapFragment" />


Fragment 생애 주기를 고려해서 처리하지 않으면 배터리 소모나 메모리 누수가 발생할 수 있으니 이런 것까지 고려해서 처리한다.

소스코드를 비교해서 보면 알겠지만 이전 게시글(https://link2me.tistory.com/1703) 코드를 거의 붙여넣기 했는데 들어갈 위치가 좀 다른 것이 있다는 점만 고려하면 된다.



activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/tabmenuLayout" />

    <LinearLayout
        android:id="@+id/tabmenuLayout"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:orientation="horizontal"
        android:layout_alignParentBottom="true" >

        <Button
            android:id="@+id/btn_tab1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="구글맵"/>

        <Button
            android:id="@+id/btn_tab2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="내 위치"/>

        <Button
            android:id="@+id/btn_tab3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Tab 3"/>


    </LinearLayout>

</RelativeLayout>


fragment_02.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000">

    <com.google.android.gms.maps.MapView
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="com.google.android.gms.maps.MapFragment"
        />

</RelativeLayout>



MainActivity.java

import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    Context context;
    private Button tab1, tab2, tab3;
    private Fragment fragment = null;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            initView();
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(context, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT).show();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = this.getBaseContext();
        checkPermissions();
    }

    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= 23) { // 마시멜로(안드로이드 6.0) 이상 권한 체크
            TedPermission.with(context)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[]{
                            android.Manifest.permission.ACCESS_FINE_LOCATION,
                            android.Manifest.permission.ACCESS_COARSE_LOCATION
                            //android.Manifest.permission.READ_EXTERNAL_STORAGE,
                            //android.Manifest.permission.WRITE_EXTERNAL_STORAGE // 기기, 사진, 미디어, 파일 엑세스 권한
                    })
                    .check();

        } else {
            initView();
        }
    }

    private void initView() {
        tab1 = findViewById(R.id.btn_tab1);
        tab2 = findViewById(R.id.btn_tab2);
        tab3 = findViewById(R.id.btn_tab3);

        tab1.setOnClickListener(this);
        tab2.setOnClickListener(this);
        tab3.setOnClickListener(this);

        if(findViewById(R.id.fragment_container) != null){
            Fragment01 fragment01 = new Fragment01();
            fragment01.setArguments(getIntent().getExtras());

            getFragmentManager().beginTransaction().add(R.id.fragment_container, fragment01).commitAllowingStateLoss();
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_tab1:
                fragment = new Fragment01();
                selectFragment(fragment);
                break;
            case R.id.btn_tab2:
                fragment = new Fragment02();
                selectFragment(fragment);
                break;
            case R.id.btn_tab3:
                fragment = new Fragment03();
                selectFragment(fragment);
                break;
        }
    }

    private void selectFragment(Fragment fragment) {
        // 액티비티 내의 프래그먼트를 관리하려면 FragmentManager를 사용해야 함.
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.fragment_container,fragment);
        // FragmentTransaction을 변경하고 나면, 반드시 commit()을 호출해야 변경 내용이 적용됨
        fragmentTransaction.commit();
    }
}


Fragment2.java

package com.link2me.android.googlemap;

import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;

import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapView;
import com.google.android.gms.maps.MapsInitializer;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;

public class Fragment02 extends Fragment implements OnMapReadyCallback {

    private FragmentActivity mContext;

    private static final String TAG = Fragment02.class.getSimpleName();
    private GoogleMap mMap;
    private MapView mapView = null;
    private Marker currentMarker = null;

    // The entry point to the Fused Location Provider.
    private FusedLocationProviderClient mFusedLocationProviderClient; // Deprecated된 FusedLocationApi를 대체
    private LocationRequest locationRequest;
    private Location mCurrentLocatiion;

    private final LatLng mDefaultLocation = new LatLng(37.56, 126.97);
    private static final int DEFAULT_ZOOM = 15;
    private static final int PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1;
    private boolean mLocationPermissionGranted;

    private static final int GPS_ENABLE_REQUEST_CODE = 2001;
    private static final int UPDATE_INTERVAL_MS = 1000 * 60 * 1;  // 1분 단위 시간 갱신
    private static final int FASTEST_UPDATE_INTERVAL_MS = 1000 * 30 ; // 30초 단위로 화면 갱신

    private static final String KEY_CAMERA_POSITION = "camera_position";
    private static final String KEY_LOCATION = "location";

    public Fragment02() {
    }

    @Override
    public void onAttach(Activity activity) { // Fragment 가 Activity에 attach 될 때 호출된다.
        mContext =(FragmentActivity) activity;
        super.onAttach(activity);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 초기화 해야 하는 리소스들을 여기서 초기화 해준다.
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // Layout 을 inflate 하는 곳이다.
        if (savedInstanceState != null) {
            mCurrentLocatiion = savedInstanceState.getParcelable(KEY_LOCATION);
            CameraPosition mCameraPosition = savedInstanceState.getParcelable(KEY_CAMERA_POSITION);
        }
        View layout =  inflater.inflate(R.layout.fragment_02,container,false);
        mapView = (MapView)layout.findViewById(R.id.map);
        if(mapView != null) {
            mapView.onCreate(savedInstanceState);
        }
        mapView.getMapAsync(this);
        return layout;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        // Fragement에서의 OnCreateView를 마치고, Activity에서 onCreate()가 호출되고 나서 호출되는 메소드이다.
        // Activity와 Fragment의 뷰가 모두 생성된 상태로, View를 변경하는 작업이 가능한 단계다.
        super.onActivityCreated(savedInstanceState);

        //액티비티가 처음 생성될 때 실행되는 함수
        MapsInitializer.initialize(mContext);

        locationRequest = new LocationRequest()
                .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) // 정확도를 최우선적으로 고려
                .setInterval(UPDATE_INTERVAL_MS) // 위치가 Update 되는 주기
                .setFastestInterval(FASTEST_UPDATE_INTERVAL_MS); // 위치 획득후 업데이트되는 주기

        LocationSettingsRequest.Builder builder =
                new LocationSettingsRequest.Builder();

        builder.addLocationRequest(locationRequest);

        // FusedLocationProviderClient 객체 생성
        mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(mContext);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        mapView.onSaveInstanceState(outState);
    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        mMap = googleMap;

        setDefaultLocation(); // GPS를 찾지 못하는 장소에 있을 경우 지도의 초기 위치가 필요함.

        getLocationPermission();

        updateLocationUI();

        getDeviceLocation();
    }

    private void updateLocationUI() {
        if (mMap == null) {
            return;
        }
        try {
            if (mLocationPermissionGranted) {
                mMap.setMyLocationEnabled(true);
                mMap.getUiSettings().setMyLocationButtonEnabled(true);
            } else {
                mMap.setMyLocationEnabled(false);
                mMap.getUiSettings().setMyLocationButtonEnabled(false);
                mCurrentLocatiion = null;
                getLocationPermission();
            }
        } catch (SecurityException e)  {
            Log.e("Exception: %s", e.getMessage());
        }
    }

    private void setDefaultLocation() {
        if (currentMarker != null) currentMarker.remove();

        MarkerOptions markerOptions = new MarkerOptions();
        markerOptions.position(mDefaultLocation);
        markerOptions.title("위치정보 가져올 수 없음");
        markerOptions.snippet("위치 퍼미션과 GPS 활성 여부 확인하세요");
        markerOptions.draggable(true);
        markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED));
        currentMarker = mMap.addMarker(markerOptions);

        CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(mDefaultLocation, 15);
        mMap.moveCamera(cameraUpdate);
    }

    String getCurrentAddress(LatLng latlng) {
        // 위치 정보와 지역으로부터 주소 문자열을 구한다.
        List<Address> addressList = null ;
        Geocoder geocoder = new Geocoder( mContext, Locale.getDefault());

        // 지오코더를 이용하여 주소 리스트를 구한다.
        try {
            addressList = geocoder.getFromLocation(latlng.latitude,latlng.longitude,1);
        } catch (IOException e) {
            Toast. makeText( mContext, "위치로부터 주소를 인식할 수 없습니다. 네트워크가 연결되어 있는지 확인해 주세요.", Toast.LENGTH_SHORT ).show();
            e.printStackTrace();
            return "주소 인식 불가" ;
        }

        if (addressList.size() < 1) { // 주소 리스트가 비어있는지 비어 있으면
            return "해당 위치에 주소 없음" ;
        }

        // 주소를 담는 문자열을 생성하고 리턴
        Address address = addressList.get(0);
        StringBuilder addressStringBuilder = new StringBuilder();
        for (int i = 0; i <= address.getMaxAddressLineIndex(); i++) {
            addressStringBuilder.append(address.getAddressLine(i));
            if (i < address.getMaxAddressLineIndex())
                addressStringBuilder.append("\n");
        }

        return addressStringBuilder.toString();
    }

    LocationCallback locationCallback = new LocationCallback() {
        @Override
        public void onLocationResult(LocationResult locationResult) {
            super.onLocationResult(locationResult);

            List<Location> locationList = locationResult.getLocations();

            if (locationList.size() > 0) {
                Location location = locationList.get(locationList.size() - 1);

                LatLng currentPosition
                        = new LatLng(location.getLatitude(), location.getLongitude());

                String markerTitle = getCurrentAddress(currentPosition);
                String markerSnippet = "위도:" + String.valueOf(location.getLatitude())
                        + " 경도:" + String.valueOf(location.getLongitude());

                Log.d(TAG, "Time :" + CurrentTime() + " onLocationResult : " + markerSnippet);

                //현재 위치에 마커 생성하고 이동
                setCurrentLocation(location, markerTitle, markerSnippet);
                mCurrentLocatiion = location;
            }
        }

    };

    private String CurrentTime(){
        Date today = new Date();
        SimpleDateFormat date = new SimpleDateFormat("yyyy/MM/dd");
        SimpleDateFormat time = new SimpleDateFormat("hh:mm:ss a");
        return time.format(today);
    }

    public void setCurrentLocation(Location location, String markerTitle, String markerSnippet) {
        if (currentMarker != null) currentMarker.remove();

        LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude());

        MarkerOptions markerOptions = new MarkerOptions();
        markerOptions.position(currentLatLng);
        markerOptions.title(markerTitle);
        markerOptions.snippet(markerSnippet);
        markerOptions.draggable(true);

        currentMarker = mMap.addMarker(markerOptions);

        CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLng(currentLatLng);
        mMap.moveCamera(cameraUpdate);
    }

    private void getDeviceLocation() {
        try {
            if (mLocationPermissionGranted) {
                mFusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper());
            }
        } catch (SecurityException e)  {
            Log.e("Exception: %s", e.getMessage());
        }
    }

    private void getLocationPermission() {
        if (ContextCompat.checkSelfPermission(mContext,
                android.Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED) {
            mLocationPermissionGranted = true;
        } else {
            ActivityCompat.requestPermissions(mContext,
                    new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                    PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String permissions[],
                                           @NonNull int[] grantResults) {
        mLocationPermissionGranted = false;
        switch (requestCode) {
            case PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION: {
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    mLocationPermissionGranted = true;
                }
            }
        }
        updateLocationUI();
    }


    public boolean checkLocationServicesStatus() {
        LocationManager locationManager = (LocationManager) getActivity().getSystemService(Context.LOCATION_SERVICE);

        return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
                locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
    }

    @Override
    public void onStart() { // 유저에게 Fragment가 보이도록 해준다.
        super.onStart();
        mapView.onStart();
        Log.d(TAG, "onStart ");
    }

    @Override
    public void onStop() {
        super.onStop();
        mapView.onStop();
        if (mFusedLocationProviderClient != null) {
            Log.d(TAG, "onStop : removeLocationUpdates");
            mFusedLocationProviderClient.removeLocationUpdates(locationCallback);
        }
    }

    @Override
    public void onResume() { // 유저에게 Fragment가 보여지고, 유저와 상호작용이 가능하게 되는 부분
        super.onResume();
        mapView.onResume();
        if (mLocationPermissionGranted) {
            Log.d(TAG, "onResume : requestLocationUpdates");
            mFusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, null);
            if (mMap!=null)
                mMap.setMyLocationEnabled(true);
        }
    }

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

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        mapView.onLowMemory();
    }

    @Override
    public void onDestroyView() { // 프래그먼트와 관련된 View 가 제거되는 단계
        super.onDestroyView();
        if (mFusedLocationProviderClient != null) {
            Log.d(TAG, "onDestroyView : removeLocationUpdates");
            mFusedLocationProviderClient.removeLocationUpdates(locationCallback);
        }
    }

    @Override
    public void onDestroy() {
        // Destroy 할 때는, 반대로 OnDestroyView에서 View를 제거하고, OnDestroy()를 호출한다.
        super.onDestroy();
        mapView.onDestroy();
    }

}


테스트한 소스 코드는 다른 기능을 좀 더 구현 테스트해보고 올려둘 생각이다.


블로그 이미지

Link2Me

,
728x90

구글 제공 구글맵 샘플코드는 https://github.com/googlemaps/android-samples/tree/master 에서 다운로드 받아서 필요한 것을 구현하면 된다.


지도 객체

- 하나의 지도는 View 요소와 인터페이스 요소로 구성된다.

- View 요소는 화면에 지도를 나타내는 역할을 하며, MapFragment 와 MapView 가 여기에 해당된다.

- 지도 화면은 Fragment 및 View로 제공된다. 이 중 한가지를 Layout 에 추가하면 화면에 지도가 나타난다.

- 지도를 화면에 나타내는 방법 중 가장 권장되는 것은 MapFragment 를 사용하는 것이다.

- Fragment 를 사용하지 않고 View를 바로 사용할 수도 있다.

  MapView를 Layout에 추가하면 MapFragment 를 사용한 것과 마찬가지로 지도가 화면에 나타난다.

- MapView를 사용할 때는 반드시 MapView가 포함된 액티비티의 라이프 사이클에 맞추어 onCreate(), onStart(), onResume(), onPause(), onStop(), onDestroy(), onSaveInstanceState(), onLowMemory()를 호출해야 합니다. 단, MapView가 프래그먼트에 포함될 경우 프래그먼트의 onCreateView() 또는 onViewCreated()에서 onCreate()를, onDestroyView()에서 onDestroy()를 호출해야 합니다. 그렇지 않으면 지도가 정상적으로 동작하지 않는다.
MapFragment를 사용하면 이러한 절차가 필요하지 않으므로 MapFragment를 사용하는 것을 권장한다.



FusedLocationProviderClient

https://developers.google.com/android/reference/com/google/android/gms/location/FusedLocationProviderClient 참조

private FusedLocationProviderClient mFusedLocationProviderClient;

// FusedLocationProviderClient 객체 생성
mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(mContext);



현재 위치를 획득하는 걸 LocationManager service = (LocationManager) getSystemService(Context.LOCATION_SERVICE); 를 활용해볼까 하고 좀 찾아봤더니 이건 오래된 방식이라고 나온다.


GPS 수신이 불가능한 곳에 위치할 경우에는 네트워크 사업자의 위치정보를 받아서 처리하는 걸 추가하려고 하는데 아직 여기까지는 처리하지 못했다.


https://webnautes.tistory.com/1249 에 나온 예제가 너무 잘 되어 있어 거의 활용하면서 구글맵 예제와 비교하면서 수정해서 테스트 해봤다.


activity_current_place.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

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

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="50dp">

            <ImageView
                android:id="@+id/navibar_bg"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:adjustViewBounds="false"
                android:scaleType="fitXY"
                android:src="@drawable/navibar_bg" />

            <TextView
                android:id="@+id/navibar_text"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="No Title"
                android:textColor="@android:color/white"
                android:textSize="22dp" />

            <Button
                android:id="@+id/list_btn"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="10dp"
                android:background="@drawable/btn_playlist" />

            <Button
                android:id="@+id/home_btn"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:layout_marginRight="10dp"
                android:background="@drawable/btn_home_c" />

        </RelativeLayout>

        <fragment xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:id="@+id/realtimemap"
            android:name="com.google.android.gms.maps.SupportMapFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </LinearLayout>

</LinearLayout>


CurrentPlace.java

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;

public class CurrentPlace extends AppCompatActivity implements View.OnClickListener, OnMapReadyCallback {
    private static final String TAG = CurrentPlace.class.getSimpleName();
    Context mContext;
    TextView title;

    private GoogleMap mMap;
    private Marker currentMarker = null;

    // The entry point to the Fused Location Provider.
    private FusedLocationProviderClient mFusedLocationProviderClient; // Deprecated된 FusedLocationApi를 대체
    private LocationRequest locationRequest;
    private Location mCurrentLocatiion;

    private final LatLng mDefaultLocation = new LatLng(37.56, 126.97);
    private static final int DEFAULT_ZOOM = 15;
    private static final int PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1;
    private boolean mLocationPermissionGranted;

    private static final int GPS_ENABLE_REQUEST_CODE = 2001;
    private static final int UPDATE_INTERVAL_MS = 1000 * 60 * 15;  // LOG 찍어보니 이걸 주기로 하지 않는듯
    private static final int FASTEST_UPDATE_INTERVAL_MS = 1000 * 30 ; // 30초 단위로 화면 갱신

    private static final String KEY_CAMERA_POSITION = "camera_position";
    private static final String KEY_LOCATION = "location";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState != null) {
            mCurrentLocatiion = savedInstanceState.getParcelable(KEY_LOCATION);
            CameraPosition mCameraPosition = savedInstanceState.getParcelable(KEY_CAMERA_POSITION);
        }
        setContentView(R.layout.activity_current_place);
        mContext = CurrentPlace.this;

        // button action...
        findViewById(R.id.list_btn).setOnClickListener(this);
        findViewById(R.id.home_btn).setOnClickListener(this);

        // title set
        title = (TextView) this.findViewById(R.id.navibar_text); // title
        title.setText("실시간 위치 표시");

        locationRequest = new LocationRequest()
                .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) // 정확도를 최우선적으로 고려
                .setInterval(UPDATE_INTERVAL_MS) // 위치가 Update 되는 주기
                .setFastestInterval(FASTEST_UPDATE_INTERVAL_MS); // 위치 획득후 업데이트되는 주기

        LocationSettingsRequest.Builder builder =
                new LocationSettingsRequest.Builder();

        builder.addLocationRequest(locationRequest);

        // Construct a FusedLocationProviderClient.
        mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this);

        // Build the map.
        SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
                .findFragmentById(R.id.realtimemap);
        mapFragment.getMapAsync(this);
    }

    /**
     * Saves the state of the map when the activity is paused.
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        if (mMap != null) {
            outState.putParcelable(KEY_CAMERA_POSITION, mMap.getCameraPosition());
            outState.putParcelable(KEY_LOCATION, mCurrentLocatiion);
            super.onSaveInstanceState(outState);
        }
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.list_btn:
                // 팝업 메뉴 나오게 하는 방법
                PopupMenu popupMenu = new PopupMenu(mContext, view);
                Menu menu = popupMenu.getMenu();
                menu.add(Menu.NONE,R.id.option_get_place,Menu.NONE, "Get Place");
                //menu.add(Menu.NONE,R.id.option_get_place,Menu.NONE, "");
                popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
                    @Override
                    public boolean onMenuItemClick(MenuItem item) {
                        switch (item.getItemId()) {
                            case R.id.option_get_place:
                                //showCurrentPlace(); // 아직 이 부분 미 구현
                                break;
                        }
                        return true;
                    }
                });
                popupMenu.show();
                break;
            case R.id.home_btn:
                Intent intent = new Intent(CurrentPlace.this, MainActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            default:
                break;
        }
    }

    @Override
    public void onMapReady(GoogleMap map) {
        Log.e(TAG, "onMapReady :");

        mMap = map;

        setDefaultLocation(); // GPS를 찾지 못하는 장소에 있을 경우 지도의 초기 위치가 필요함.

        getLocationPermission();

        updateLocationUI();

        getDeviceLocation();
    }

    private void updateLocationUI() {
        if (mMap == null) {
            return;
        }
        try {
            if (mLocationPermissionGranted) {
                mMap.setMyLocationEnabled(true);
                mMap.getUiSettings().setMyLocationButtonEnabled(true);
            } else {
                mMap.setMyLocationEnabled(false);
                mMap.getUiSettings().setMyLocationButtonEnabled(false);
                mCurrentLocatiion = null;
                getLocationPermission();
            }
        } catch (SecurityException e)  {
            Log.e("Exception: %s", e.getMessage());
        }
    }

    private void setDefaultLocation() {
        if (currentMarker != null) currentMarker.remove();

        MarkerOptions markerOptions = new MarkerOptions();
        markerOptions.position(mDefaultLocation);
        markerOptions.title("위치정보 가져올 수 없음");
        markerOptions.snippet("위치 퍼미션과 GPS 활성 여부 확인하세요");
        markerOptions.draggable(true);
        markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED));
        currentMarker = mMap.addMarker(markerOptions);

        CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(mDefaultLocation, 15);
        mMap.moveCamera(cameraUpdate);
    }

    String getCurrentAddress(LatLng latlng) {
        // 위치 정보와 지역으로부터 주소 문자열을 구한다.
        List<Address> addressList = null ;
        Geocoder geocoder = new Geocoder( this, Locale.getDefault());

        // 지오코더를 이용하여 주소 리스트를 구한다.
        try {
            addressList = geocoder.getFromLocation(latlng.latitude,latlng.longitude,1);
        } catch (IOException e) {
            Toast. makeText( this, "위치로부터 주소를 인식할 수 없습니다. 네트워크가 연결되어 있는지 확인해 주세요.", Toast.LENGTH_SHORT ).show();
            e.printStackTrace();
            return "주소 인식 불가" ;
        }

        if (addressList.size() < 1) { // 주소 리스트가 비어있는지 비어 있으면
            return "해당 위치에 주소 없음" ;
        }

        // 주소를 담는 문자열을 생성하고 리턴
        Address address = addressList.get(0);
        StringBuilder addressStringBuilder = new StringBuilder();
        for (int i = 0; i <= address.getMaxAddressLineIndex(); i++) {
            addressStringBuilder.append(address.getAddressLine(i));
            if (i < address.getMaxAddressLineIndex())
                addressStringBuilder.append("\n");
        }

        return addressStringBuilder.toString();
    }

    LocationCallback locationCallback = new LocationCallback() {
        @Override
        public void onLocationResult(LocationResult locationResult) {
            super.onLocationResult(locationResult);

            List<Location> locationList = locationResult.getLocations();

            if (locationList.size() > 0) {
                Location location = locationList.get(locationList.size() - 1);

                LatLng currentPosition
                        = new LatLng(location.getLatitude(), location.getLongitude());

                String markerTitle = getCurrentAddress(currentPosition);
                String markerSnippet = "위도:" + String.valueOf(location.getLatitude())
                        + " 경도:" + String.valueOf(location.getLongitude());

                Log.d(TAG, "Time :" + CurrentTime() + " onLocationResult : " + markerSnippet);

                // Update 주기를 확인해보려고 시간을 찍어보았음.
                //현재 위치에 마커 생성하고 이동
                setCurrentLocation(location, markerTitle, markerSnippet);
                mCurrentLocatiion = location;
            }
        }

    };

    private String CurrentTime(){
        Date today = new Date();
        SimpleDateFormat date = new SimpleDateFormat("yyyy/MM/dd");
        SimpleDateFormat time = new SimpleDateFormat("hh:mm:ss a");
        return time.format(today);
    }

    public void setCurrentLocation(Location location, String markerTitle, String markerSnippet) {
        if (currentMarker != null) currentMarker.remove();

        LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude());

        MarkerOptions markerOptions = new MarkerOptions();
        markerOptions.position(currentLatLng);
        markerOptions.title(markerTitle);
        markerOptions.snippet(markerSnippet);
        markerOptions.draggable(true);

        currentMarker = mMap.addMarker(markerOptions);

        CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLng(currentLatLng);
        mMap.moveCamera(cameraUpdate);
    }

    private void getDeviceLocation() {
        try {
            if (mLocationPermissionGranted) {
                mFusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper());
            }
        } catch (SecurityException e)  {
            Log.e("Exception: %s", e.getMessage());
        }
    }

    private void getLocationPermission() {
        if (ContextCompat.checkSelfPermission(this.getApplicationContext(),
                android.Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED) {
            mLocationPermissionGranted = true;
        } else {
            ActivityCompat.requestPermissions(this,
                    new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                    PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String permissions[],
                                           @NonNull int[] grantResults) {
        mLocationPermissionGranted = false;
        switch (requestCode) {
            case PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION: {
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    mLocationPermissionGranted = true;
                }
            }
        }
        updateLocationUI();
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (mLocationPermissionGranted) {
            Log.d(TAG, "onStart : requestLocationUpdates");
            mFusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, null);
            if (mMap!=null)
                mMap.setMyLocationEnabled(true);
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mFusedLocationProviderClient != null) {
            Log.d(TAG, "onStop : removeLocationUpdates");
            mFusedLocationProviderClient.removeLocationUpdates(locationCallback);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mFusedLocationProviderClient != null) {
            Log.d(TAG, "onDestroy : removeLocationUpdates");
            mFusedLocationProviderClient.removeLocationUpdates(locationCallback);
        }
    }
}


앱 build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28


    defaultConfig {
        applicationId "com.link2me.android.googlemap"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        // multiDexEnabled true // minSdkVersion 이 21 이상인 경우 사용
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    //implementation 'com.google.android.gms:play-services-places:16.0.0' // OLD
    //implementation 'com.google.android.libraries.places:places:2.0.0'  // NEW
    implementation 'com.google.android.libraries.places:places-compat:2.0.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'
    implementation 'com.github.bumptech.glide:glide:3.8.0' // 이미지 라이브러리
    implementation 'androidx.multidex:multidex:2.0.0' // minSdkVersion 이 21 이하인 경우 사용
    implementation 'com.google.android.material:material:1.2.0'
}


최근에는 minSdkVersion 을 23 이상으로 하라는 코드가 상당히 많다.

보안이 강화되면서 minSdkVersion 버전을 올리는 것이 좋은 듯하다.

블로그 이미지

Link2Me

,
728x90

구글맵에 현재 위치 표시하기


Activity 에서 현재 위치를 표시해주는 것은 구글맵에 나온 예제를 그대로 실행하면 잘 된다.

Fragment 에서 실행하려고 시도했는데 에러가 발생한다.

Fragment 에 대한 이해 부족인 거 같아서 다른 예제를 좀 찾아서 시도해봐야 할 거 같다.


https://developers.google.com/maps/documentation/android-sdk/current-place-tutorial


AndroidManifext.xml 파일 내용은 https://link2me.tistory.com/1700 하단 내용 참조하면 된다.


activity_current_place.xml

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MapsActivityCurrentPlace">

    <fragment xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>


custom_info_contents.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layoutDirection="locale"
    android:orientation="vertical">
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textColor="#ff000000"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/snippet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#ff7f7f7f" />
</LinearLayout>


current_place_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/option_get_place"
        android:title="Get Place"
        app:showAsAction="always" />
</menu>


the options menu 를 사용하지 않고 별도의 타이틀바에서 처리되도록 만들기 위해서 코드를 아래 코든는 삭제했다.

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.current_place_menu, menu);
        return true;
    }
 

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.option_get_place) {
            showCurrentPlace();
        }
        return true;
    }


위 코드 대신에 추가한 코드는

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.list_btn:
                // 팝업 메뉴 나오게 하는 방법
                PopupMenu popupMenu = new PopupMenu(mContext, view);
                Menu menu = popupMenu.getMenu();
                menu.add(Menu.NONE,R.id.option_get_place,Menu.NONE, "Get Place");
                //menu.add(Menu.NONE,R.id.option_get_place,Menu.NONE, "");
                popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
                    @Override
                    public boolean onMenuItemClick(MenuItem item) {
                        switch (item.getItemId()) {
                            case R.id.option_get_place:
                                showCurrentPlace();
                                break;
                        }
                        return true;
                    }
                });
                popupMenu.show();
                break;
            case R.id.home_btn:
                Intent intent = new Intent(MapsActivityCurrentPlace.this, MainActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            default:
                break;
        }
    }




MapsActivityCurrentPlace.java

구글 맵 예제로 제공하는 사항이 코드를 구현해보면 The Google Play Services Places SDK is deprecated. 라고 나온다.

찾아보니 2019.7.29일 서비스가 종료되었단다.

해결방안은 앱 build.gradle 에서

//implementation 'com.google.android.gms:play-services-places:16.0.0' // OLD
implementation 'com.google.android.libraries.places:places-compat:2.0.0'

를 해주면 된다고 나와 컴파일 해보니 잘 된다. 아직은 OLD 로 해도 잘 되더라.


package com.link2me.android.googlemap;

import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.places.GeoDataClient;
import com.google.android.gms.location.places.PlaceDetectionClient;
import com.google.android.gms.location.places.PlaceLikelihood;
import com.google.android.gms.location.places.PlaceLikelihoodBufferResponse;
import com.google.android.gms.location.places.Places;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;

public class MapsActivityCurrentPlace extends AppCompatActivity implements View.OnClickListener, OnMapReadyCallback {
    // 현재 위치를 구글 지도에 표시하는 예제
    // https://developers.google.com/maps/documentation/android-sdk/current-place-tutorial 참조
    private static final String TAG = MapsActivityCurrentPlace.class.getSimpleName();
    Context mContext;
    TextView title;

    private GoogleMap mMap;
    private CameraPosition mCameraPosition;

    // The entry points to the Places API.
    private GeoDataClient mGeoDataClient;
    private PlaceDetectionClient mPlaceDetectionClient;

    // The entry point to the Fused Location Provider.
    private FusedLocationProviderClient mFusedLocationProviderClient;

    // A default location (Sydney, Australia) and default zoom to use when location permission is not granted.
    private final LatLng mDefaultLocation = new LatLng(-33.8523341, 151.2106085);
    private static final int DEFAULT_ZOOM = 15;
    private static final int PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1;
    private boolean mLocationPermissionGranted;

    // The geographical location where the device is currently located. That is, the last-known
    // location retrieved by the Fused Location Provider.
    private Location mLastKnownLocation;

    // Keys for storing activity state.
    private static final String KEY_CAMERA_POSITION = "camera_position";
    private static final String KEY_LOCATION = "location";

    // Used for selecting the current place.
    private static final int M_MAX_ENTRIES = 5;
    private String[] mLikelyPlaceNames;
    private String[] mLikelyPlaceAddresses;
    private String[] mLikelyPlaceAttributions;
    private LatLng[] mLikelyPlaceLatLngs;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Retrieve location and camera position from saved instance state.
        if (savedInstanceState != null) {
            mLastKnownLocation = savedInstanceState.getParcelable(KEY_LOCATION);
            mCameraPosition = savedInstanceState.getParcelable(KEY_CAMERA_POSITION);
        }
        setContentView(R.layout.activity_maps_current_place);
        mContext = MapsActivityCurrentPlace.this;

        // button action...
        findViewById(R.id.list_btn).setOnClickListener(this);
        findViewById(R.id.home_btn).setOnClickListener(this);

        // title set
        title = (TextView) this.findViewById(R.id.navibar_text); // title
        title.setText(getIntent().getExtras().getString("title"));

        // Construct a GeoDataClient.
        mGeoDataClient = Places.getGeoDataClient(this, null);

        // Construct a PlaceDetectionClient.
        mPlaceDetectionClient = Places.getPlaceDetectionClient(this, null);

        // Construct a FusedLocationProviderClient.
        mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this);

        // Build the map.
        SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
                .findFragmentById(R.id.map);
        mapFragment.getMapAsync(this);

    }

    /**
     * Saves the state of the map when the activity is paused.
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        if (mMap != null) {
            outState.putParcelable(KEY_CAMERA_POSITION, mMap.getCameraPosition());
            outState.putParcelable(KEY_LOCATION, mLastKnownLocation);
            super.onSaveInstanceState(outState);
        }
    }

    /**
     * Manipulates the map when it's available.
     * This callback is triggered when the map is ready to be used.
     */
    @Override
    public void onMapReady(GoogleMap map) {
        mMap = map;

        // Use a custom info window adapter to handle multiple lines of text in the info window contents.
        mMap.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {

            @Override
            // Return null here, so that getInfoContents() is called next.
            public View getInfoWindow(Marker arg0) {
                return null;
            }

            @Override
            public View getInfoContents(Marker marker) {
                // Inflate the layouts for the info window, title and snippet.
                View infoWindow = getLayoutInflater().inflate(R.layout.custom_info_contents,
                        (FrameLayout) findViewById(R.id.map), false);

                TextView title = ((TextView) infoWindow.findViewById(R.id.title));
                title.setText(marker.getTitle());

                TextView snippet = ((TextView) infoWindow.findViewById(R.id.snippet));
                snippet.setText(marker.getSnippet());

                return infoWindow;
            }
        });

        // Prompt the user for permission.
        getLocationPermission();

        // Turn on the My Location layer and the related control on the map.
        updateLocationUI();

        // Get the current location of the device and set the position of the map.
        getDeviceLocation();
    }

    /**
     * Gets the current location of the device, and positions the map's camera.
     */
    private void getDeviceLocation() {
        /*
         * Get the best and most recent location of the device, which may be null in rare
         * cases when a location is not available.
         */
        try {
            if (mLocationPermissionGranted) {
                Task<Location> locationResult = mFusedLocationProviderClient.getLastLocation();
                locationResult.addOnCompleteListener(this, new OnCompleteListener<Location>() {
                    @Override
                    public void onComplete(@NonNull Task<Location> task) {
                        if (task.isSuccessful()) {
                            // Set the map's camera position to the current location of the device.
                            mLastKnownLocation = task.getResult();
                            mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(
                                    new LatLng(mLastKnownLocation.getLatitude(),
                                            mLastKnownLocation.getLongitude()), DEFAULT_ZOOM));
                        } else {
                            Log.d(TAG, "Current location is null. Using defaults.");
                            Log.e(TAG, "Exception: %s", task.getException());
                            mMap.moveCamera(CameraUpdateFactory
                                    .newLatLngZoom(mDefaultLocation, DEFAULT_ZOOM));
                            mMap.getUiSettings().setMyLocationButtonEnabled(false);
                        }
                    }
                });
            }
        } catch (SecurityException e)  {
            Log.e("Exception: %s", e.getMessage());
        }
    }


    /**
     * Prompts the user for permission to use the device location.
     */
    private void getLocationPermission() {
        /*
         * Request location permission, so that we can get the location of the
         * device. The result of the permission request is handled by a callback,
         * onRequestPermissionsResult.
         */
        if (ContextCompat.checkSelfPermission(this.getApplicationContext(),
                android.Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED) {
            mLocationPermissionGranted = true;
        } else {
            ActivityCompat.requestPermissions(this,
                    new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                    PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION);
        }
    }

    /**
     * Handles the result of the request for location permissions.
     */
    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String permissions[],
                                           @NonNull int[] grantResults) {
        mLocationPermissionGranted = false;
        switch (requestCode) {
            case PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION: {
                // If request is cancelled, the result arrays are empty.
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    mLocationPermissionGranted = true;
                }
            }
        }
        updateLocationUI();
    }

    /**
     * Prompts the user to select the current place from a list of likely places, and shows the
     * current place on the map - provided the user has granted location permission.
     */
    private void showCurrentPlace() {
        if (mMap == null) {
            return;
        }

        if (mLocationPermissionGranted) {
            // Get the likely places - that is, the businesses and other points of interest that
            // are the best match for the device's current location.
            @SuppressWarnings("MissingPermission") final
            Task<PlaceLikelihoodBufferResponse> placeResult =
                    mPlaceDetectionClient.getCurrentPlace(null);
            placeResult.addOnCompleteListener
                    (new OnCompleteListener<PlaceLikelihoodBufferResponse>() {
                        @Override
                        public void onComplete(@NonNull Task<PlaceLikelihoodBufferResponse> task) {
                            if (task.isSuccessful() && task.getResult() != null) {
                                PlaceLikelihoodBufferResponse likelyPlaces = task.getResult();

                                // Set the count, handling cases where less than 5 entries are returned.
                                int count;
                                if (likelyPlaces.getCount() < M_MAX_ENTRIES) {
                                    count = likelyPlaces.getCount();
                                } else {
                                    count = M_MAX_ENTRIES;
                                }

                                int i = 0;
                                mLikelyPlaceNames = new String[count];
                                mLikelyPlaceAddresses = new String[count];
                                mLikelyPlaceAttributions = new String[count];
                                mLikelyPlaceLatLngs = new LatLng[count];

                                for (PlaceLikelihood placeLikelihood : likelyPlaces) {
                                    // Build a list of likely places to show the user.
                                    mLikelyPlaceNames[i] = (String) placeLikelihood.getPlace().getName();
                                    mLikelyPlaceAddresses[i] = (String) placeLikelihood.getPlace()
                                            .getAddress();
                                    mLikelyPlaceAttributions[i] = (String) placeLikelihood.getPlace()
                                            .getAttributions();
                                    mLikelyPlaceLatLngs[i] = placeLikelihood.getPlace().getLatLng();

                                    i++;
                                    if (i > (count - 1)) {
                                        break;
                                    }
                                }

                                // Release the place likelihood buffer, to avoid memory leaks.
                                likelyPlaces.release();

                                // Show a dialog offering the user the list of likely places, and add a
                                // marker at the selected place.
                                openPlacesDialog();

                            } else {
                                Log.e(TAG, "Exception: %s", task.getException());
                            }
                        }
                    });
        } else {
            // The user has not granted permission.
            Log.i(TAG, "The user did not grant location permission.");

            // Add a default marker, because the user hasn't selected a place.
            mMap.addMarker(new MarkerOptions()
                    .title("기본 위치")
                    .position(mDefaultLocation)
                    .snippet("No places found, because location is disabled."));

            // Prompt the user for permission.
            getLocationPermission();
        }
    }

    /**
     * Displays a form allowing the user to select a place from a list of likely places.
     */
    private void openPlacesDialog() {
        // Ask the user to choose the place where they are now.
        DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // The "which" argument contains the position of the selected item.
                LatLng markerLatLng = mLikelyPlaceLatLngs[which];
                String markerSnippet = mLikelyPlaceAddresses[which];
                if (mLikelyPlaceAttributions[which] != null) {
                    markerSnippet = markerSnippet + "\n" + mLikelyPlaceAttributions[which];
                }

                // Add a marker for the selected place, with an info window showing information about that place.
                mMap.addMarker(new MarkerOptions()
                        .title(mLikelyPlaceNames[which])
                        .position(markerLatLng)
                        .snippet(markerSnippet));

                // Position the map's camera at the location of the marker.
                mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(markerLatLng,
                        DEFAULT_ZOOM));
            }
        };

        // Display the dialog.
        AlertDialog dialog = new AlertDialog.Builder(this)
                .setTitle("장소를 선택하세요.")
                .setItems(mLikelyPlaceNames, listener)
                .show();
    }

    /**
     * Updates the map's UI settings based on whether the user has granted location permission.
     */
    private void updateLocationUI() {
        if (mMap == null) {
            return;
        }
        try {
            if (mLocationPermissionGranted) {
                mMap.setMyLocationEnabled(true);
                mMap.getUiSettings().setMyLocationButtonEnabled(true);
            } else {
                mMap.setMyLocationEnabled(false);
                mMap.getUiSettings().setMyLocationButtonEnabled(false);
                mLastKnownLocation = null;
                getLocationPermission();
            }
        } catch (SecurityException e)  {
            Log.e("Exception: %s", e.getMessage());
        }
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.list_btn:
                // 팝업 메뉴 나오게 하는 방법
                PopupMenu popupMenu = new PopupMenu(mContext, view);
                Menu menu = popupMenu.getMenu();
                menu.add(Menu.NONE,R.id.option_get_place,Menu.NONE, "");
                //menu.add(Menu.NONE,R.id.option_get_place,Menu.NONE, "");
                popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
                    @Override
                    public boolean onMenuItemClick(MenuItem item) {
                        switch (item.getItemId()) {
                            case R.id.option_get_place:
                                showCurrentPlace();
                                break;
                        }
                        return true;
                    }
                });
                popupMenu.show();
                break;
            case R.id.home_btn:
                Intent intent = new Intent(MapsActivityCurrentPlace.this, MainActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            default:
                break;
        }
    }
}



블로그 이미지

Link2Me

,
728x90

https://developers.google.com/maps/documentation/android-sdk/get-api-key 를 참조한다.


구글 맵을 사용하기 위해서는 먼저 Google API Key 를 발급받아야 한다.


1. https://console.developers.google.com/ 에 접속한다.


2. 프로젝트를 신규로 만들거나 이미 만들어진 프로젝트를 선택한다.

   - 이미 만들어진 프로젝트 Contacts 를 선택했다.

     프로젝트를 연습한다고 신규로 계속 만들었는데 그럴 필요가 없다.


3. 왼쪽 메뉴에서 라이브러리를 선택한다.

   - Maps SDK for Android 를 선택한다.

   - 사용설정을 누른다.


4. 사용자 인증정보 만들기

    - 구글 Firebase Message 를 사용하기 위해 등록하면서 만들어진 사용자 인증키가 있더라.


   - 내용을 확인해보니 Server key 가 Firebase Cloud Message 에서 사용하는 키(이전 서버키)더라.

   - Android key 를 눌러서 선택했다.

    - 항목 추가를 누른다.

① 패키지 이름은 B를 복사하여 붙여넣기 한다.



② SHA-1 인증서 디지털 지문

     - A를 눌러서 복사한다.

     - Android Studio 가 설치된 경로에 있는 jre/bin 폴더에 keytool.exe 파일이 존재한다.

     - 이 폴더에서 cmd 창 상태로 전환해서 A 복사한 걸 붙여넣기 하고 엔터키를 친다.

     - SHA-1 키를 복사하여 붙여넣기 한다.

       만약 Drag 가 안된다면 파일로 저장해서 Copy & Paste 하면 된다.

       회사 컴과 집 컴이 각각 다르다면 SHA-1 이 다를 것이므로 항목 추가로 각각 등록해서 테스트하면 된다.

      - 패키지 이름과 SHA-1 인증서 디지털 지문을 복사하여 붙여넣기를 한 다음 완료를 누른다

      - 저장을 누르면 Key 발급 준비가 된 것이다.


이제 바로 위의 A를 눌러서 복사한 API 키를

AndroidManifest.xml 파일에 구글 API Key를 등록한다.

키는 임의로 변경한 것이 있으므로 실제 키와는 다르다.

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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><!-- GPS -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><!-- WiFi or mobile -->

    <application
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MapsActivityCurrentPlace"></activity>

        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />

        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="AIzaSyAgfzQZqx7nFVFhwKAhviwRP5Rmi9bmZPE" />

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

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    implementation 'com.google.android.gms:play-services-places:16.0.0'
    implementation 'com.google.android.libraries.places:places:2.0.0'

}
 


이제 구글맵을 사용할 준비는 끝났다.

블로그 이미지

Link2Me

,
728x90

구글 캘린더를 연동하기 위한 API 사용 설정 과정이다.







7번 항목은 http://link2me.tistory.com/1406 참조하면 구할 수 있다.





10번 항목을 구글과 관련된 명칭으로 하면 에러가 나면서 Client ID가 생성되지 않는다는 걸 확인했다.



아래 그림은 개발용 PC/노트북이 다른 환경(집, 사무실)에서 등록하는 방법은 추가를 하는 것이다.








AndroidManifest.xml


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

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />

    <application
        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=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />

    </application>

</manifest>


블로그 이미지

Link2Me

,
728x90

안드로이드 어플 사용자에게 PUSH 메시지를 발송하기 위해서는 PHP 서버에서 관련 코드를 구현해야 한다.

앱이 백그라운드에서 실행중이거나 앱이 죽어있을 때에도 동작하게 하려면 FCM API를 직접 호출해주는 방식을 사용해야 한다.


api url :: https://fcm.googleapis.com/fcm/send


https://console.firebase.google.com/ 에 접속한다.





서버키 4번을 복사해서 아래 코드에 붙여넣기를 한다.


=== fcmpush.php ====

 <?php
require_once 'dbconnect.php'; // db접속
//데이터베이스에 접속해서 토큰들을 가져와서 FCM에 발신요청
$sql = "select tokenID From member_data where LENGTH(tokenID)>63";
$result = mysqli_query($dbconn,$sql);
$tokens = array();
if(mysqli_num_rows($result) > 0 ){
    while ($row = mysqli_fetch_assoc($result)) {
        $tokens[] = $row['tokenID'];
    }
} else {
    echo 'There are no Transfer Datas';
    exit;
}

mysqli_close($dbconn);

$title = isset($_POST['title'])? $_POST['title'] : "PUSH TEST";
$message = isset($_POST['message'])? $_POST['message'] : "새글이 등록되었습니다";

$arr = array();
$arr['title'] = $title;
$arr['message'] = $message;

$message_status = Android_Push($tokens, $arr);
//echo $message_status;
// 푸시 전송 결과 반환.
$obj = json_decode($message_status);

// 푸시 전송시 성공 수량 반환.
$cnt = $obj->{"success"};

echo $cnt;

function Android_Push($tokens, $message) {
    $url = 'https://fcm.googleapis.com/fcm/send';
    $apiKey = "서버키";

    $fields = array('registration_ids' => $tokens,'data' => $message);
    $headers = array('Authorization:key='.$apiKey,'Content-Type: application/json');

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
    $result = curl_exec($ch);
    if ($result === FALSE) {
        die('Curl failed: ' . curl_error($ch));
    }
    curl_close($ch);
    return $result;
}
?>


=== fcmsender.php 샘플 파일 ===

<!DOCTYPE html>
<head>
<title>FCM Sender</title>
    <meta charset="utf-8" />
</head>
<body>
    <form action="fcmpush.php" method="post">
        <table style="width:100;padding:0;margin:0px;border:none">
        <tr>
            <th style="width:20%">제목</th>
            <td style="width:80%"><input type="text" name="title" value="" style="width:100%"></td>
        </tr>
        <tr>
            <th>내용</th>
            <td>
            <textarea name="message" rows=5 cols=42></textarea>
            </td>
        </tr>
        <tr>
            <td colspan=2 style="text-align: center;"><input type="submit" value="Send Notification" /></td>
        </tr>
        </table>
    </form>
</body>
</html>


FCM PUSH PHP 서버 샘플 소스를 첨부한다.


FCM_Push_Sample.zip


블로그 이미지

Link2Me

,
728x90

last Update : 2019.9.2

사용자 기기의 고유 토큰 정보를 획득하는 방법이 변경되었다.

// 사용자 기기의 고유 토큰 정보를 획득
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener( Login.this,  new OnSuccessListener<InstanceIdResult>() {
    @Override
    public void onSuccess(InstanceIdResult instanceIdResult) {
        newToken = instanceIdResult.getToken();
        Log.e("newToken",newToken);

    }
});

Button submit = (Button) findViewById(R.id.login_btn);
submit.setOnClickListener(new Button.OnClickListener() {
    @Override
    public void onClick(View view) {
        loginID = etId.getText().toString().trim();
        loginPW = etPw.getText().toString().trim();

        if (loginID != null && !loginID.isEmpty() && loginPW != null && !loginPW.isEmpty()) {
            Uri.Builder builder = new Uri.Builder()
                    .appendQueryParameter("loginID", Value.encrypt(loginID))
                    .appendQueryParameter("loginPW", Value.encrypt(loginPW))
                    .appendQueryParameter("uID", Value.encrypt(Value.getPhoneUID(context)))
                    .appendQueryParameter("AppVersion", Value.VERSION)
                    .appendQueryParameter("phoneVersion", Build.VERSION.RELEASE)
                    .appendQueryParameter("phoneBrand", Build.BRAND)
                    .appendQueryParameter("phoneModel", Build.MODEL)
                    .appendQueryParameter("mfoneNo", Value.encrypt(getPhoneNumber()))
                    .appendQueryParameter("tokenID", newToken)
                    .appendQueryParameter("keyword", Value.encrypt(Value.URLkey()));
            String urlParameters = builder.build().getEncodedQuery();

            new AsyncLogin().execute(Value.IPADDRESS + "/loginChk.php", urlParameters);
        }
    }
});


========================================================================

Update : 2017.9.2


안드로이드 스튜디오에서 FCM 관련 코드를 구현했으면 이제 구글에서 생성해주는 토큰을 PHP 서버에 등록해야 한다.


토큰(Token)을 등록하는 방법은 로그인 정보에 같이 포함해서 보내는 것이 가장 편리하다.


// 단말기의 ID 정보를 얻기 위해서는 READ_PHONE_STATE 권한이 필요
TelephonyManager mTelephony = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
if (mTelephony.getDeviceId() != null){
    uID = mTelephony.getDeviceId();  // 스마트폰 기기 정보
} else {
    uID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
}

// 사용자 기기의 고유 토큰 정보를 획득
String getToken = FirebaseInstanceId.getInstance().getToken();

System.out.println("DeviceID : " + uID);
System.out.println("Intro Token : " + getToken);

// 전달할 인자들
String keyword = Value.URLkey();
Uri.Builder builder = new Uri.Builder()
        .appendQueryParameter("loginID", Value.encrypt(params[0]))
        .appendQueryParameter("loginPW", Value.encrypt(params[1]))
        .appendQueryParameter("uID", Value.encrypt(uID))
        .appendQueryParameter("phoneVersion", Build.VERSION.RELEASE)
        .appendQueryParameter("phoneBrand", Build.BRAND)
        .appendQueryParameter("phoneModel", Build.MODEL)
        .appendQueryParameter("tokenID", getToken)
        .appendQueryParameter("keyword", Value.encrypt(Value.URLkey()));
String urlParameters = builder.build().getEncodedQuery();
 


토큰에 대한 정보를 획득하여 로그인하면서 서버로 전송하면 된다.


이제 PHP 서버에서는 이 정보를 사용자 DB에 저장해야 한다.


loginChk.php 파일에서 안드로이드 접속 인증 체크를 한다고 가정하자.

loginID, loginPW 인증에 성공하면

// 토큰 등록 및 갱신
if(isset($tokenID) && strlen($tokenID) > 63){
    $c->registerToken($tokenID,$loginID,$deviceID); // DB에 토큰 저장
}


함수를 포함시켜서 DB에 토큰 정보가 저장되고 Update 되도록 한다.

아래 코드는 토큰 저장에 필요한 함수만 발췌했다.

<?php
class TokenClass {
    // 토큰 등록 및 갱신
    function registerToken($token,$userID,$deviceID){
        global $dbconn;
        $rs = $this->getDeviceChk($userID,$deviceID); // memberuid 또는 0
        if($rs > 0 ){
            $gettokenID = $this->getTokenIDChk($rs); // 0 또는 DB 등록 토큰 반환
            if(($gettokenID == '0') || ($gettokenID !== $token)){ // 등록안되었거나 토큰 불일치
                $sql ="UPDATE member_data SET tokenID='".$token."'";
                $sql.=" where memberuid='".$rs."'";
                if($result = mysqli_query($dbconn,$sql)){
                    return 1;
                } else {
                    return 0;
                }
            }
        }
    }

    function getTokenIDChk($rs){
        global $dbconn;
        $sql ="select tokenID from member_data where memberuid='".$rs."'";
        if($result = mysqli_query($dbconn,$sql)){
            $row = mysqli_fetch_row($result);
            if($row[0] == NULL){
                return 0;
            } else {
                return $row[0];
            }
        } else {
            return 0;
        }
    }

    function getDeviceChk($userID,$deviceID){
        global $dbconn;
        // 사용자 기준 장치 정보 검사
        $sql ="select count(phoneID),memberuid from member_data";
        $sql.=" where memberuid=(select uid from member_id where id='".$userID."') and phoneID='".$deviceID."'";
        if($result = mysqli_query($dbconn,$sql)){
            $row = mysqli_fetch_row($result);
            if($row[0] == 1){
                return $row[1]; // 있으면 memberuid 반환
            } else {
                return $row[0];
            }
        } else {
            return 0;
        }
    }

}//end class
?>
 



DB 테이블 구조

테이블 구조는 참조하여 필요한 것만 발췌하여 이용하면 된다.

member_id.uid = member_data.memberuid


CREATE TABLE IF NOT EXISTS `member_data` (
  `memberuid` int(11) NOT NULL,
  `site` int(11) NOT NULL DEFAULT '0',
  `auth` tinyint(4) NOT NULL DEFAULT '0',
  `level` int(11) NOT NULL DEFAULT '0',
  `admin` tinyint(4) NOT NULL DEFAULT '0',
  `email` varchar(50) NOT NULL DEFAULT '',
  `name` varchar(30) NOT NULL DEFAULT '',
  `nic` varchar(50) NOT NULL DEFAULT '',
  `grade` varchar(20) NOT NULL DEFAULT '',
  `photo` varchar(200) NOT NULL DEFAULT '',
  `sex` tinyint(4) NOT NULL DEFAULT '0',
  `officeNO` varchar(14) NOT NULL DEFAULT '',
  `mobileNO` varchar(14) NOT NULL DEFAULT '',
  `num_login` int(11) NOT NULL DEFAULT '0',
  `d_regis` varchar(14) NOT NULL DEFAULT '',
  `tokenID` varchar(200) DEFAULT NULL,
  `phoneID` varchar(60) DEFAULT NULL,
  `phoneVersion` varchar(20) DEFAULT NULL,
  `phoneBrand` varchar(20) DEFAULT NULL,
  `phoneModel` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`memberuid`),
  KEY `site` (`site`),
  KEY `auth` (`auth`),
  KEY `level` (`level`),
  KEY `admin` (`admin`),
  KEY `email` (`email`),
  KEY `name` (`name`),
  KEY `nic` (`nic`),
  KEY `sex` (`sex`),
  KEY `d_regis` (`d_regis`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;


CREATE TABLE IF NOT EXISTS `member_id` (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `site` int(11) NOT NULL DEFAULT '0',
  `id` varchar(50) NOT NULL DEFAULT '',
  `pw` varchar(50) NOT NULL DEFAULT '',
  `code` int(6) NOT NULL DEFAULT '0',
  `admin` int(2) NOT NULL DEFAULT '0',
  PRIMARY KEY (`uid`),
  UNIQUE KEY `id` (`id`),
  KEY `code` (`code`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;


블로그 이미지

Link2Me

,
728x90

안드로이드에서 구현할 사항은 newToken 생성하여 DB에 저장되도록 하는 것, PUSH 메시지를 수신하면 팝업된 메시지 창을 누르면 해당 게시글로 이동하여 해당 게시글을 읽어서 내용을 확인하도록 하는 것이 주요 목적이다.


last Update : 2019.8.31

https://firebase.google.com/docs/cloud-messaging/android/client?authuser=0 를 참고하여 파일을 구현한다.


https://medium.com/android-school/firebaseinstanceidservice-is-deprecated-50651f17a148

FirebaseInstanceIdService is deprecated 내용이 나온다.

이걸 참조해서 newToken 생성하는 코드를 추가한다. (다음 게시글에 코드 추가했음)


AndroidManifest.xml 파일 수정사항

<service
    android:name=".FCMListenerService"
    android:stopWithTask="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
 


FCMListenerService.java 파일

public class FCMListenerService extends FirebaseMessagingService {
    private static final String TAG = "FCM";

    @Override
    public void onNewToken(String s) {
        super.onNewToken(s);
        Log.d(TAG, "token[" + s + "]" );
        /*
         * 기존의 FirebaseInstanceIdService에서 수행하던 토큰 생성, 갱신 등의 역할은 이제부터
         * FirebaseMessaging에 새롭게 추가된 위 메소드를 사용하면 된다.
         */
    }

    // [START receive_message]
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // 메시지를 받았을 때 동작하는 메소드
        String title = remoteMessage.getData().get("title");
        String message = remoteMessage.getData().get("message");
        Log.d(TAG, "From: " + remoteMessage.getFrom());
        Log.d(TAG, "Title: " + title);
        Log.d(TAG, "Message: " + message);

        NotificationHelper notificationHelper = new NotificationHelper(getApplicationContext());
        notificationHelper.createNotification(title,message);
    }
}


Android 8.0(API 수준 26) 이상부터는 알림 채널이 지원 및 권장된다. FCM은 기본적인 설정으로 기본 알림 채널을 제공한다.


NotificationHelper 는 https://link2me.tistory.com/1514 코드를 참조하면 된다.


===========================================================================

Update : 2017.9.2

FCM(Firebase Clouding Messaging) 을 서버(PHP)에서 PUSH 메시지를 보내면 안드로이드폰에서 받기 위해서는 관련 코드를 구현해야 한다.


GCM 에서 FCM 으로 Migrate 하는 방법에 대해서는 https://developers.google.com/cloud-messaging/android/android-migrate-fcm 에 자세히 설명되어 있다.


1. 토큰 등록 코드

파일명은 본인이 지정하고 싶은데로 지정하면 된다.

이 코드에 추가해서 member id 에 연계하여 토큰정보를 수집하도록 해도 되고, 로그인 코드에 추가해도 된다.


public class FCMInstanceIDListenerService extends FirebaseInstanceIdService {
private static final String TAG = "MyFirebaseIDService";

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


2. FCM 메시지를 수신 받아서 처리하는 코드

    보통 메시지를 수신하면 메시지 제목만 팝업 메시지로 띄우고,

    클릭하면 메시지 내용을 볼 수 있는 UI로 이동하도록 코드를 구현한다.

 public class FCMListenerService extends com.google.firebase.messaging.FirebaseMessagingService {
    private static final String TAG = "FCM";

    int mLastId = 0;
    ArrayList<Integer> mActiveIdList = new ArrayList<Integer>();
    NotificationManager nm;

    // [START receive_message]
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // 메시지를 받았을 때 동작하는 메소드
        String title = remoteMessage.getData().get("title");
        String message = remoteMessage.getData().get("message");
        Log.d(TAG, "From: " + remoteMessage.getFrom());
        Log.d(TAG, "Title: " + title);
        Log.d(TAG, "Message: " + message);

        sendPushNotification(title,message);
    }

    private void createNotificationId() {
        int id = ++mLastId;
        mActiveIdList.add(id);
    }

    public void sendPushNotification(String title, String message) {
        nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        nm.cancel(mLastId);
        createNotificationId();

        Uri defaultSoundUri= RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

        // 하위 호환성을 위해 NotificationCompat.Builder 사용

        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.mipmap.icon)
                .setContentTitle(title)
                .setSound(defaultSoundUri)
                .setLights(000000255,500,2000)
                .setAutoCancel(true)
                .setWhen(System.currentTimeMillis())
                .setContentText(message);

        Intent popupIntent = new Intent(getApplicationContext(), Popup_Noti.class);
        popupIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        popupIntent.putExtra("msg", title);
        popupIntent.putExtra("LastId", mLastId);
        startActivity(popupIntent); // 메시지 팝업창을 바로 띄운다.

        PendingIntent resultPendingIntent =PendingIntent.getActivity(this, 0, popupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        mBuilder.setContentIntent(resultPendingIntent);

        nm.notify(mLastId, mBuilder.build());
    }
}


getActivity(Context, int, Intent, int) -> Activity를 시작하는 인텐트를 생성함
getBroadcast(Context, int, Intent, int) -> BroadcastReceiver를 시작하는 인텐트를 생성함
getService(Context, int, Intent, int)  -> Service를 시작하는 인텐트를 생성함


사용자가 알림 창의 알림 텍스트를 클릭했을 때 Activity를 시작하려면, setContentIntent()를 호출하여 PendingIntent를 추가한다.


참조 : https://developer.android.com/guide/topics/ui/notifiers/notifications?hl=ko



 public class Popup_Noti extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        requestWindowFeature(Window.FEATURE_NO_TITLE);
        // 키잠금 해제 및 화면 켜기
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);

        AlertDialog.Builder builder = new AlertDialog.Builder(Popup_Noti.this);
        builder.setTitle(R.string.app_name);  // 앱의 이름
        builder.setMessage(getIntent().getStringExtra("msg")); // 넘겨받은 메시지 제목
        builder.setCancelable(false);
        builder.setPositiveButton("내용 보기", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Intent intent = new Intent(Popup_Noti.this, Notice.class); // 공지사항 정보 팝업 및 파싱처리
                startActivityForResult(intent, 0);
                NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                nm.cancel(getIntent().getIntExtra("LastId", -1));
                finish();
            }
        });
        builder.setNegativeButton("닫기", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Intent intent = new Intent(Popup_Noti.this, Intro.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
                        | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
                finish();
            }
        });
        AlertDialog alertDialog = builder.create();
        alertDialog.show();
    }
}


3. AndroidManifest.xml 파일에 추가할 사항

 <service android:name="com.tistory.link2me.fcm.FCMListenerService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
<service android:name="com.tistory.link2me.fcm.FCMInstanceIDListenerService">
    <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
    </intent-filter>
</service>




블로그 이미지

Link2Me

,
728x90

last Update : 2019.8.30


Firebase 클라우드 메시징(FCM)은 메시지를 무료로 안정적으로 전송할 수 있는 교차 플랫폼 메시징 솔루션이다.

구글이 PUSH 메시징 플랫폼을 GCM(Google Cloud Message) 에서 FCM(Firebase Cloud Message) 로 변경을 권고하면서 구글에서 GCM 메시지 전송을 위한 설정 세팅 정보 연결을 찾기가 어렵다. (2017.4.11 기준)

안드로이드 9.0 부터는 GCM 은 동작하지 않는다.


FCM(Firebase Cloud Message) 를 활용하여 PUSH 알림 메시지를 구현하고자 한다.

- 공지 전송 전용 게시판에 글을 등록하여 지정된 회원에게 PUSH 메시지 전송

- 서버 게시판에서 특정 글을 등록하면 지정된 회원에게 PUSH 메시지 자동 전송



https://firebase.google.com/docs/cloud-messaging/ 에 동영상으로 설명이 나오고, 주요기능 및 작동원리, 구현경로에 대한 설명이 잘되어 있다.


가장 먼저 해야 할 일은 구글 계정에서 FCM 등록을 하는 법이다.


1. https://console.firebase.google.com/ 에 접속한다.

   프로젝트를 추가한다.  




안드로이드 앱의 패키지명을 입력한다.


위 그림에서 잘 보면 파일의 위치가 해당 앱 폴더 아래에 위치한다.

해당 앱 모듈의 build.gradle 이 있는 폴더에 google-services.json 파일을 넣어주어야 한다.



모듈 앱에 빠진 부분을 추가한다.

https://firebase.google.com/docs/android/setup 에 가면 가장 최신 Firebase 라이브러리 파일 정보가 나온다.


프로젝트 수준 build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
   
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        classpath 'com.google.gms:google-services:4.2.0'       
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}


앱 build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    buildToolsVersion = '28.0.3'

    defaultConfig {
        applicationId "com.link2me.android.contact"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'
    implementation 'com.github.bumptech.glide:glide:3.8.0' // 이미지 라이브러리
    implementation 'com.google.firebase:firebase-core:16.0.8'
    implementation 'com.google.firebase:firebase-messaging:19.0.1'

}

apply plugin: 'com.google.gms.google-services'  // Google Play services Gradle plugin


gradle.properties

android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx1536m


These two lines automatically resolved my dependency conflicts between google's files and third party dependencies.

두 줄을 추가하면 구글 파일과 서드파티 dependencies 사이의 충돌이 나서 빨간 줄 표시나는 걸 해결해준다.


이제 안드로이드 앱에서 구현할 소스와 PHP(Web) 소스를 구현하면 된다.

블로그 이미지

Link2Me

,
728x90

2019.5월 구글에서 GCM removed 되어 FCM 으로 코딩 변경해야 사용 가능하다.

-----------------------------------------------------------------------------------------------------


구글 GCM(Google Cloud Message) 를 이용한 PUSH 메시지 전송을 하려면 https://console.developers.google.com 에서 API 를 등록하고 사용자를 등록해야 한다.


PUSH 메시지 개념은 서버에서 보내는 메시지를 스마트폰에서 받는 것으로 구글에서 제공하는 GCM(Google Cloud Message) 서비스를 이용한다.


현재는 FCM 으로 등록하라고 URL 을 제공하기 때문에 GCM 등록은 쉽지 않다.

FCM 으로 메시지 전송하는 걸 제대로 하려면 PHP 기반 게시판까지 연동해야 될 거 같아서 테스트하고 정리까지 하기에는 시간이 좀 걸릴거 같다.




사용자정보 등록방법



동일한 키를 가지고 서버 IP만 여러개 등록하면 어플 여러개에서 GCM 을 사용할 수 있다.



구글에서 등록할 기능은 위와같이 하면 끝난다.


GCM Sender.php 파일 일부 내용

function AndroidPush($registrationIDs){
    global $message;

    $apiKey ="AIzaSyCoq9ccKiIrovwqm4mY0Ss"; // 생성코드(일부 내용 수정했음)
    $url = 'https://android.googleapis.com/gcm/send';
    $headers = array( 'Authorization: key=' . $apiKey, 'Content-Type: application/json' );

    $data = array('registration_ids' => $registrationIDs, 'data' => array( "msg" => $message ) );

    // Open connection
    $ch = curl_init();

    // Set the url, number of POST vars, POST data
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_URL,$url);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

    // Execute post
    $reponse = curl_exec($ch);

    // Close connection
    curl_close($ch);
    $_rs=json_decode($reponse,true);
    return $_rs['success'];
}


Android 코드에서 사용할 서비스 계정 ID








블로그 이미지

Link2Me

,