Android는 내부적으로 결제를 할 수 있도록, API를 제공하고 있습니다. 이 문서는 인앱 결제에 대한 설명만을 포함하고 있습니다. 우선 앱이 프로덕션 혹은 베타테스트 출시 상태에서 보시면 좋으리라 생각합니다.

 

[선행작업]

출시용 APK 파일 만들기 : https://nicgoon.tistory.com/198
베타 버전으로 앱 등록하기 : https://nicgoon.tistory.com/199

 

[공식 문서]

안드로이드 개발자 사이트 : https://developer.android.com/
안드로이드 개발자 사이트 공식 문서 루트 : https://developer.android.com/docs
안드로이드 빌링 공식 문서 개요 : https://developer.android.com/google/play/billing/billing_overview

 

[주요 사이트]

구글 플레이 콘솔 : https://play.google.com/apps/publish

 

[참고 사이트]

샘플 소스 : https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive_v2
안드로이드 인앱 결제 구현 : https://jizard.tistory.com/137

 

 

 

1. 인앱 결제 종류.

구글 에서는 인앱 결제에 대해서, 3가지 종류를 지원 합니다. 첫 번째는 1회성 결제를 하는 상품 결제, 구독을 하는 결제, 그리고, 중간 광고등을 보고 그 에 대한 보상을 처리하는 보상형 결제 등이 있습니다. 이 문서는 틈틈히 수정되고 업데이트 될 것 입니다. 첫 번째로, 1회성 결제를 설명할 것이고, 그 외에 것들은 차차 업데이트를 하게 될 것 같습니다.

 

2. 인앱 결제 권한 추가 및 결제 라이브러리 추가.

먼저 위 선행작업을 완료하였거나, 그에 준하는 경험을 가지고 있다고, 보고 설명을 시작합니다. 

먼저, 구글 플레이 콘솔에가 APK 파일을 업로드 하고, 알파, 베타, 프로덕션 어느 버전이든, 출시를 시작합니다. 앱을 선택하고 왼쪽 메뉴 (출시 하기전 왼쪽 메뉴와 모습이 틀립니다.) 중 앱정보 > 인앱 상품 항목을 선택하면, 다음과 같은 문구를 볼 수 있습니다. 이미 APK 파일을 업로드 했지만, 인앱 결제 권한이 없어 항목을 사용할 수 없습니다. 

그런 이유로, 인앱 결제 권한을 가진 APK 파일을 업로드 한 뒤에야 (알파나 베타 출시까지 할 필요는 없고, apk 파일만 업로드 하면됨) 비로서, 인앱 상품을 등록할 수 있습니다.

 

먼저, 앱레벨 build.gradle 파일을 열어 dependecy 에 독립 모듈이 추가될 수 있도록, 아래와 같은 구문을 추가합니다.

dependencies {
    ...
    implementation 'com.android.billingclient:billing:1.2.2'
}

 

그 다음, AndroidManifest.xml 파일을 열어, 아래의 권한을 추가합니다.

<uses-permission android:name="com.android.vending.BILLING" />

 

빌드 후 APK 파일을 새로 등록하고, 새 버전으로 출시 합니다.

 

3. 관리되는 제품 등록.(왼쪽 메뉴의 앱 정보 > 인앱 상품, 선택)

 

관리되는 제품 > 관리되는 제품 만들기를 차례로 눌러, 아래와 같이 1회성 상품을 등록할 수 있는 페이지를 호출 합니다. 호출된 페이지에서, 적당한 값을 넣어, 상품을 만들어 줍니다. 주의 할 점은 상태 값인데, 활성 APK 상태로 두어야 상품이 구매 가능한 상태가 됩니다. 모든 값을 입력했다면, 저장 버튼을 눌러 줍니다.

 

다시 왼쪽 메뉴의 앱정보 > 인앱 상품을 선택하면, 상품이 추가된 것을 볼 수 있습니다.

 

4. 결제 클래스 생성.

관련문서 : https://developer.android.com/google/play/billing/billing_library_overview?hl=ko

결제 관련 모듈을 클래스를 따로 생성해서 호출 하는 식으로 사용해야 합니다. 꼭 예제를 보면서 만드세요. 예제속 Shared-module > BillingManager.java 파일속에 해당 내용이 들어 있습니다.

PurchasesUpdatedListener 를 상속한 결제 메니저, BillingManager 클래스를 생성합니다. (implements 로 다중 상속을 합니다. - 그렇게 안하면 오류남. ) 그리고, 빌링 클라이언트 객체를 생성하면, onBillingSetupFinished 메소드가 호출 됩니다. 문서에는 이 부분이 완료되면, 이 후 결제 작업을 준비 한다고 되어 있습니다.

해당 클래스에 일단 onPurchasesUpdated 메소드를 재정의 해 줍니다. (필수 재정의 메소드).

public class BillingManager implements PurchasesUpdatedListener {
    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {

    }
}

아래는 billingClient를 생성하고, 구글 플레이 스토어와 연결이 되었을 때, 처리하는 코드 구글 플레이 스토어와 상태가 해제 되었을 때 처리하는 코드 입니다. 상속한 onBillingServiceDisconnected 메소드는 구글 플레이 스토어와의 접속을 잃었을 때 호출된다고 합니다. 이 때는 startConnection() 메소드를 재 호출 해 줘야 한다고 합니다. 그래서 접속이 끊어 졌는 지 확인할 수 있는 변수, isConnected 가 필요합니다. 공식예제 에서는 결제 모듈을 쓰레드에서 Rennable에서 처리를 하고 있습니다.

일단 저는 메뉴얼 대로 메인 쓰레드에서 처리하도록 구현할 것 입니다. 그래서 아래와 같은 소스가 됩니다. 여기까지 소스를 입력하고, 한 번 실행해 보면, 오류 코드3을 만나고 접속에 실패할 수 있습니다. 이 이유는 실제 기기에서만, 결제 모듈이 동작 하기 때 문입니다. (이 것 때문에 1시간을 헤메임).

public class BillingManager implements PurchasesUpdatedListener {


    // 필요 상수들.
    final String TAG = "IN-APP-BILLING";


    // 초기화 시 입력 받거나 생성되는 멤버 변수들.
    private BillingClient mBillingClient;
    private Activity mActivity;


    // 현재 접속 상태를 나타 냅니다.
    public enum connectStatusTypes { waiting, connected, fail, disconnected }
    public connectStatusTypes connectStatus = connectStatusTypes.waiting;



    // 생성자.
    public BillingManager( Activity _activity )
    {


        // 초기화 시 입력 받은 값들을 넣어 줍니다.
        mActivity = _activity;


        Log.d(TAG, "구글 결제 매니저를 초기화 하고 있습니다.");

        // 결제를 위한, 빌링 클라이언트를 생성합니다.
        mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build();


        // 구글 플레이와 연결을 시도합니다.
        mBillingClient.startConnection(new BillingClientStateListener() {

            // 결제 작업 완료 가능한 상태.
            @Override
            public void onBillingSetupFinished(int responseCode) {

                // 접속이 성공한 경우, 처리.
                if (responseCode == BillingClient.BillingResponse.OK) {
                    connectStatus = connectStatusTypes.connected;
                    Log.d(TAG, "구글 결제 서버에 접속을 성공하였습니다.");
                }

                // 접속이 실패한 경우, 처리.
                else {

                    connectStatus = connectStatusTypes.fail;
                    Log.d(TAG, "구글 결제 서버 접속에 실패하였습니다.\n오류코드:" + responseCode);
                }

            }

            // 결제 작업 중, 구글 서버와 연결이 끊어진 상태.
            @Override
            public void onBillingServiceDisconnected() {

                connectStatus = connectStatusTypes.disconnected;
                Log.d(TAG, "구글 결제 서버와 접속이 끊어졌습니다.");

            }


        });


    }


    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {

    }
}

위 소스를 이용해 클래스를 만들 고 있을 액티비티에서 호출 하면됩니다.

 

구글 결제 시스템이 초기화 되면, 구글에 등록된 상품의 정보를 요청할 수 있습니다. 이를 통해 해당 아이디들이 유효한지 확인할 수 있습니다. String List를 이용해 Sku 배열을 만들고, 요청할 상품들의 아이디를 추가해 줍니다. 그리고, 해당 상품들의 자세한 정보를 받아 오면됩니다.

구글 플레이 콘솔로가 등록된 상품들의 정보를 확인합니다. (물론 자신이 등록했으므로, 알고 있을 경우, 굳이 확인하지 않아도 됩니다.) 구글 플레이 콘솔 접속, 왼쪽메뉴에서 앱 정보 > 인앱 상품 > 관리되는 제품을 차례로 선택하면 등록된 상품을 확인할 수 있습니다.

그리고, 지금까지 만들어 왔던 빌드 매니저에, 구입 가능한 상품의 정보를 받아 오는 코드를 추가 합니다.


    // 구입 가능한 상품의 리스트를 받아 오는 메소드 입니다.
    public void get_Sku_Detail_List()
    {


        // 구글의 상품 정보들의 ID를 만들어 줍니다.
        List<String> Sku_ID_List = new ArrayList<>();
        Sku_ID_List.add( "gold_100" );


        // SkuDetailsParam 객체를 만들어 줍니다. (1회성 소모품에 대한)
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList( Sku_ID_List ).setType(BillingClient.SkuType.INAPP);


        // 비동기 상태로 앱의, 정보를 가지고 옵니다.
        mBillingClient.querySkuDetailsAsync(params.build() , new SkuDetailsResponseListener()
                {

                    @Override
                    public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {


                        // 상품 정보를 가지고 오지 못한 경우, 오류를 반환하고 종료합니다.
                        if( responseCode != BillingClient.BillingResponse.OK)
                        {
                            Log.d(TAG, "상품 정보를 가지고 오던 중 오류를 만났습니다. 오류코드 : " + responseCode);
                            return;
                        }

                        // 하나의 상품 정보도 가지고 오지 못했습니다.
                        if( skuDetailsList == null )
                        {
                            Log.d(TAG, "상품 정보가 존재하지 않습니다.");
                            return;
                        }


                        // 응답 받은 데이터들의 숫자를 출력 합니다.
                        Log.d(TAG, "응답 받은 데이터 숫자:" + skuDetailsList.size());

                        // 받아 온 상품 정보를 차례로 호출 합니다.
                        for( int _sku_index = 0;_sku_index < skuDetailsList.size(); _sku_index++)
                        {

                            // 해당 인덱스의 객체를 가지고 옵니다.
                            SkuDetails _skuDetail = skuDetailsList.get( _sku_index );

                            // 해당 인덱스의 상품 정보를 출력하도록 합니다.
                            Log.d( TAG, _skuDetail.getSku() + ": " + _skuDetail.getTitle() + ", " + _skuDetail.getPrice() + ", " + _skuDetail.getDescription() );

                        }


                        // 받은 값을 멤버 변수로 저장합니다.
                        mSkuDetailsList = skuDetailsList;


                    }
                }
        );



    }

그리고, 실행해 보면, 지정한 상품의 정보를 받아 올 수 있습니다. 당연한 이야기 지만 구글 결제 시스템과 접속이 성공적으로 이뤄진 상태에서 실행해야 합니다.

 

드디어 결제를 요청하는 부분 입니다. 당연한 이야기지만, 우리는 콜백만 받으면, 되므로, 결제는 사실상 끝입니다. 앞서 받았던, List<SkuDetails> 에서 구매를 원하는 혹은 유저가 선택한, SkuDetails 을 선택해 결제 요청창을 띄워 주시면 됩니다. 공식 메뉴얼에는 launchBillingFlow 에 1개의 인수만 넣도록 되어 있지만, 실제로 해 보면, 첫 인수에 Activity를 요청하고 있습니다. 위 단계들을 모두 거친뒤 요청해야 합니다.

    // 실제 구입 처리를 하는 메소드 입니다.
    public void purchase( SkuDetails skuDetails )
    {
        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build();
        int responseCode = mBillingClient.launchBillingFlow(mActivity, flowParams);
    }

이 코드를 실행해 보면 아래와 같은 결제 창을 보실 수 있을 것 입니다.

 

처음 결제 클래스를 만들 때, PurchasesUpdatedListener 클래스를 상속하기 위해, 기본적으로 정의 해 두었던, onPurchasesUpdated 메소드가 생각이 나시나요? 이 메소드는 결제 처리 결과를 받는 메소드이며, 이제 이 것을 정의 하므로써, 결제 부분은 코드가 끝이 납니다. 이 메소드가 호출되는 시점은, 유저가 결제를 하던, 결제 실패를 하던, 어떤 이유로 결제가 실패해 결제 창이 닫히는 경우 발생합니다. 코드는 간단하므로, 더 설명 드리지 않겠습니다.

    // 결제 처리를 하는 메소드.
    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {


        // 결제에 성공한 경우.
        if( responseCode == BillingClient.BillingResponse.OK && purchases != null )
        {

            Log.d( TAG, "결제에 성공했으며, 아래에 구매한 상품들이 나열될 것 입니다." );

            for( Purchase _pur : purchases )
            {
                Log.d( TAG, "결제에 대해 응답 받은 데이터 :"+ _pur.getOriginalJson() );
            }
        }

        // 사용자가 결제를 취소한 경우.
        else if( responseCode == BillingClient.BillingResponse.USER_CANCELED )
        {
            Log.d( TAG, "사용자에 의해 결제가 취소 되었습니다." );
        }

        // 그 외에 다른 결제 실패 이유.
        else
        {
            Log.d( TAG, "결제가 취소 되었습니다. 종료코드 : " + responseCode );
        }



    }

 

5. 소모품 소모.

한 번 구매후 효과가 영구히 지속되는 경우, 아이템을 한 번만 구매해, 이 과정이 필요 없습니다. 하지만 대부분의 경우, 아이템을 재 구매해야 하므로, 소모품을 소모 하지않으면 아이템의 재 구매가 불가능합니다. 그냥 아이템 구매후 소모품을 소모하도록 하면되며, 비동기 처리를 하는 메소드 이므로, 아주 편리 합니다.

1) 소모품 소모후 콜백 메소드.

콜백 메소드를 처리하는 ConsumeResponseListener 객체를 멤벼 번수로 선언하고, 이것을 생성자에서 정의해 주도록 합니다.

// 멤버 변수로 선언해 줍니다.
private ConsumeResponseListener mConsumResListnere;



// 생성자에 리스너를 정의해 변수에 저장합니다.
mConsumResListnere = new ConsumeResponseListener() {
    @Override
    public void onConsumeResponse(int responseCode, String purchaseToken) {

        // 성공적으로 아이템을 소모한 경우.
        if( responseCode == BillingClient.BillingResponse.OK )
        {
            Log.d( TAG, "상품을 성공적으로 소모하였습니다. 소모된 상품=>" + purchaseToken );
            return;
        }

        // 성공적으로 아이템을 소모한 경우.
        else
        {
            Log.d( TAG, "상품 소모에 실패하였습니다. 오류코드 ("+responseCode+"), 대상 상품 코드:" + purchaseToken );
            return;
        }



    }
};

 

2) 결제 후 소모 메소드 호출 하기.

기존의 결제 리스너에, 성공 메시지가 오면, 소모품으로 아이템을 소모 하도록 호출합니다.

    // 결제 처리를 하는 메소드.
    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {


        // 결제에 성공한 경우.
        if( responseCode == BillingClient.BillingResponse.OK && purchases != null )
        {

            Log.d( TAG, "결제에 성공했으며, 아래에 구매한 상품들이 나열될 것 입니다." );

            for( Purchase _pur : purchases )
            {
                Log.d( TAG, "결제에 대해 응답 받은 데이터 :"+ _pur.getOriginalJson() );


                mBillingClient.consumeAsync( _pur.getPurchaseToken(), mConsumResListnere );

            }
        }

        // 사용자가 결제를 취소한 경우.
        else if( responseCode == BillingClient.BillingResponse.USER_CANCELED )
        {
            Log.d( TAG, "사용자에 의해 결제가 취소 되었습니다." );
        }

        // 그 외에 다른 결제 실패 이유.
        else
        {
            Log.d( TAG, "결제가 취소 되었습니다. 종료코드 : " + responseCode );
        }



    }

 

실행을 해 보면, 매우 결제가 잘 됩니다. 이 후 이 결제가 유효한 결제인지 서버 혹은 이 앱내에서 하게 됩니다. 앱내에서 검증은 신뢰할 수 없고, 저는 필요치 않아 다음에 서버 검증에 대한 글을 쓰게 될 것 같습니다. 앱내 검증이 필요하신 분은 아래 문서의 후반부를 참고 하시기 바랍니다.

 

Use the Google Play Billing Library  |  Android Developers

This document explains how to add Google Play Billing into your app using the Google Play Library. Specifically, this document covers how to add Google Play Billing functionality that's common to all in-app product types: one-time products, rewarded produc

developer.android.com

 

 

그럼 빌링매니저 풀 소스를 남겨 드립니다. 많은 분들에게 도움이 되었으면 좋겠습니다.

package com.tistory.nicgoon.android.inapptest.billing;

import android.app.Activity;
import android.support.annotation.Nullable;
import android.util.Log;

import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;

import java.util.ArrayList;
import java.util.List;

public class BillingManager implements PurchasesUpdatedListener {


    // 필요 상수들.
    final String TAG = "IN-APP-BILLING";


    // 초기화 시 입력 받거나 생성되는 멤버 변수들.
    private BillingClient mBillingClient;
    private Activity mActivity;
    private ConsumeResponseListener mConsumResListnere;


    // 현재 접속 상태를 나타 냅니다.
    public enum connectStatusTypes { waiting, connected, fail, disconnected }
    public connectStatusTypes connectStatus = connectStatusTypes.waiting;


    // 결제를 위해 가지온 상품 정보를 저장한 변수 입니다.
    public List<SkuDetails> mSkuDetailsList;


    // 생성자.
    public BillingManager( Activity _activity )
    {


        // 초기화 시 입력 받은 값들을 넣어 줍니다.
        mActivity = _activity;


        Log.d(TAG, "구글 결제 매니저를 초기화 하고 있습니다.");

        // 결제를 위한, 빌링 클라이언트를 생성합니다.
        mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build();


        // 구글 플레이와 연결을 시도합니다.
        mBillingClient.startConnection(new BillingClientStateListener() {

            // 결제 작업 완료 가능한 상태.
            @Override
            public void onBillingSetupFinished(int responseCode) {

                // 접속이 성공한 경우, 처리.
                if (responseCode == BillingClient.BillingResponse.OK) {
                    connectStatus = connectStatusTypes.connected;
                    Log.d(TAG, "구글 결제 서버에 접속을 성공하였습니다.");
                }

                // 접속이 실패한 경우, 처리.
                else {

                    connectStatus = connectStatusTypes.fail;
                    Log.d(TAG, "구글 결제 서버 접속에 실패하였습니다.\n오류코드:" + responseCode);
                }

            }

            // 결제 작업 중, 구글 서버와 연결이 끊어진 상태.
            @Override
            public void onBillingServiceDisconnected() {

                connectStatus = connectStatusTypes.disconnected;
                Log.d(TAG, "구글 결제 서버와 접속이 끊어졌습니다.");

            }


        });



        // 소모성 상품을 소모한 후, 응답 받는 메소드 입니다.
        mConsumResListnere = new ConsumeResponseListener() {
            @Override
            public void onConsumeResponse(int responseCode, String purchaseToken) {

                // 성공적으로 아이템을 소모한 경우.
                if( responseCode == BillingClient.BillingResponse.OK )
                {
                    Log.d( TAG, "상품을 성공적으로 소모하였습니다. 소모된 상품=>" + purchaseToken );
                    return;
                }

                // 성공적으로 아이템을 소모한 경우.
                else
                {
                    Log.d( TAG, "상품 소모에 실패하였습니다. 오류코드 ("+responseCode+"), 대상 상품 코드:" + purchaseToken );
                    return;
                }



            }
        };

    }


    // 구입 가능한 상품의 리스트를 받아 오는 메소드 입니다.
    public void get_Sku_Detail_List()
    {


        // 구글의 상품 정보들의 ID를 만들어 줍니다.
        List<String> Sku_ID_List = new ArrayList<>();
        Sku_ID_List.add( "gold_500" );


        // SkuDetailsParam 객체를 만들어 줍니다. (1회성 소모품에 대한)
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList( Sku_ID_List ).setType(BillingClient.SkuType.INAPP);


        // 비동기 상태로 앱의, 정보를 가지고 옵니다.
        mBillingClient.querySkuDetailsAsync(params.build() , new SkuDetailsResponseListener()
                {

                    @Override
                    public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {


                        // 상품 정보를 가지고 오지 못한 경우, 오류를 반환하고 종료합니다.
                        if( responseCode != BillingClient.BillingResponse.OK)
                        {
                            Log.d(TAG, "상품 정보를 가지고 오던 중 오류를 만났습니다. 오류코드 : " + responseCode);
                            return;
                        }

                        // 하나의 상품 정보도 가지고 오지 못했습니다.
                        if( skuDetailsList == null )
                        {
                            Log.d(TAG, "상품 정보가 존재하지 않습니다.");
                            return;
                        }


                        // 응답 받은 데이터들의 숫자를 출력 합니다.
                        Log.d(TAG, "응답 받은 데이터 숫자:" + skuDetailsList.size());

                        // 받아 온 상품 정보를 차례로 호출 합니다.
                        for( int _sku_index = 0;_sku_index < skuDetailsList.size(); _sku_index++)
                        {

                            // 해당 인덱스의 객체를 가지고 옵니다.
                            SkuDetails _skuDetail = skuDetailsList.get( _sku_index );

                            // 해당 인덱스의 상품 정보를 출력하도록 합니다.
                            Log.d( TAG, _skuDetail.getSku() + ": " + _skuDetail.getTitle() + ", " + _skuDetail.getPrice() + ", " + _skuDetail.getDescription() );
                            Log.d( TAG, _skuDetail.getOriginalJson() );


                        }


                        // 받은 값을 멤버 변수로 저장합니다.
                        mSkuDetailsList = skuDetailsList;


                    }
                }
        );



    }


    // 실제 구입 처리를 하는 메소드 입니다.
    public void purchase( SkuDetails skuDetails )
    {
        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build();
        int responseCode = mBillingClient.launchBillingFlow(mActivity, flowParams);
    }


    // 결제 처리를 하는 메소드.
    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {


        // 결제에 성공한 경우.
        if( responseCode == BillingClient.BillingResponse.OK && purchases != null )
        {

            Log.d( TAG, "결제에 성공했으며, 아래에 구매한 상품들이 나열될 것 입니다." );

            for( Purchase _pur : purchases )
            {
                Log.d( TAG, "결제에 대해 응답 받은 데이터 :"+ _pur.getOriginalJson() );


                mBillingClient.consumeAsync( _pur.getPurchaseToken(), mConsumResListnere );

            }
        }

        // 사용자가 결제를 취소한 경우.
        else if( responseCode == BillingClient.BillingResponse.USER_CANCELED )
        {
            Log.d( TAG, "사용자에 의해 결제가 취소 되었습니다." );
        }

        // 그 외에 다른 결제 실패 이유.
        else
        {
            Log.d( TAG, "결제가 취소 되었습니다. 종료코드 : " + responseCode );
        }



    }







}

 

 

 

 

 

Posted by 창업닉군
,

android 앱을 만든 뒤 실제 상품으로 등록되는 Production 버전으로 앱을 등록하면, 우리가 앱을 테스트하는 동안 실제 다운로드등이 일어나는 문제가 발생합니다. 특히 in-app billing 상품을 구매할 때, 실제 구매가 일어나, 테스트틀 위해 인앱 상품을 구해매 플레이스토어에 수수료를 물거나, 환불해야 할 지도 모릅니다. 이런 문제를 해결하기 위해, 구글플레이 스토어에서는 Alpha, Beta 버전이후 실제 제품을 출시하는 Production 단계로 업그레이드 하는 시스템을 가지고 있습니다.

Alpha : 비 공개 테스트를 위한 시스템
Beta : 공개 테스트를 위한 시스템.

저는 구매 테스트를 하기 위해 베타 버전으로 앱을 등록할 예정이고, 이 과정을 블로그에 남겨 둡니다. 다른 분들도 이 글이 도움이 되었으면 합니다.

 

1. 등록을 위한 간단한 앱 만들기.

앱을 만드는 방법은 생략합니다 저는 간단히, 버튼을 누르면 안녕하세요. 라는 메시지가 표시되는 앱을 만들었습니다.

 

2. 빌드하기. 

안드로이드 스튜디오 상단 메뉴의 빌드 항목을 이용해 빌드를 시도합니다. 처음 빌드를 하신다면, 아래 링크를 참고해 주세요.

 

android 출시용 apk 파일 만들기

iOS에 비하면 안드로이드는 출시 과정이 매우 간단한 편 입니다. 안드로이드 스튜디오 기준으로 설명을 하도록 하겠습니다. 1. 키스토어 생성 키스토어는 일종의 도장같은 파일 입니다. 이 것을 통해, 앱의 코드를..

nicgoon.tistory.com

 

3. 플레이 스토어에 애플리케이션 만들기.

이 앱을 플레이 스토어에 앱으로 등록하기 위해 플레이 스토어 내에도 애플리케이션을 만들어 주어야 합니다. 플레이스토어에 접속해, [애플리케이션 만들기] 버튼을 눌러 주도록 합니다. (저는 많은 앱들 중 리젝된 앱이 있어, 경고가 떠 있지만, 여러분은 그런 창이 아마도 없을 것 입니다.)

그리고, 호출되는 창에 값들을 적당히 입력하고, 만들기 버튼을 입력해 줍니다.

그리고 나면 아래와 같이 스토어 등록정보를 입력하는 창이 나타납니다.  꼼꼼히 확인해서, 입력해 줍니다. 모든 입력사항을 입력하고, 임시 저장을 눌러 다음 페이지로 넘어 갑니다. 단 *콘텐츠 등급은 작성하지 않고 넘어 갑니다.

 

4. APK 파일 업로드.

플레이 콘솔의 왼쪽 메뉴의 앱 버전 버튼을 눌러 줍니다.

이제 앱버전 페이지가 보이는데 우리는 공개 트랙(베타) 항목에 apk 파일을 업로드 할 것 입니다. 해당 항목의 관리 버튼을 눌러 주도록 합니다.

베타 창에서 새 버전 출시하기 버튼을 눌러 새 버전을 출시하도록 합니다.

구글에서 앱 서명 키 관리 미 보호 옵션이 뜨는 데, 그냥 계속을 눌러 줍니다.

그럼 라이센스 관련 창이 뜨는데 언제나 처럼 다음으로 넘어 가줍니다. 그럼 그 아래 apk를 등록할 수 있는 항목이 활성화 됩니다. 여기서 중요한 것은 apk 파일을 업로드하는 것 입니다. 붉은 색으로 표시한 파일 찾아보기 버튼을 눌러 APK 파일을 추가한 뒤 그 아래 항목은 적당한 값들로 채워 넣습니다. 그 후 저장 버튼을 작성한 문서가 일단 저장됩니다. 그리고 검토 버튼이 활성화 되면, 검토 버튼을 눌러 다음 단계로 진행을 해 주도록 합니다.

저는 다음 화면으로 왔을 때 경고가 떴습니다. 일단 어떤 경고인지 확인해 보겠습니다. 경고 메시지 보기 버튼을 눌러 줍니다.

테스터가 한 명도 등록되지 않아 발생한 오류 입니다.

 

5. 테스터 설정하기.

구글 플레이 콘솔에서, 앱버전을 눌러 나온 창에서, 공개 트랙의 관리 버튼을 눌러 주도록 합니다.

그러면, 테스트 참여 대상 관리 항목이 나오는데, 테스트 방법 선택에 공개 베타 테스트를 선택하고, 최대 테스터 수를 1000명으로 합니다. 그리고 저장 버튼을 눌러 줍니다.

저장이 완료 되면, 그 아래 버전 수정 버튼을 눌러 다음 페이지로 이동합니다.

앱 버전 창이 나오면 검토 버튼을 눌러, 다음으로 이동하도록 합니다.

다음 창에서 확인해 보면, 앱 버전을 출시 준비가 되었지만, 아직 베타 출시 시작 버튼이 활성화 되지 않았습니다.

 

6. 콘텐츠 등급

왼쪽 메뉴의 콘텐츠 등급 버튼을 눌러 나온 페이지에 계속 버튼을 눌러 콘텐츠 등급을 작성합니다. 

아래와 같은 창이 뜨면, 이메일 주소를 입력하고, 앱 카테고리 선택 항목에서, 참고자료, 뉴스 또는 교육을 선택합니다. (물론 여러분이 필요에 의해 작성할 때는 상황에 맞는 카테고리를 선택하면 되겠습니다.)

설문지 창이 나오면, 적절한 항목을 선택한 뒤, 설문지 저장 버튼을 눌러 줍니다.

설문지 저장 버튼을 누르고, 저장이 완료되면, 등급계산 버튼이 활성화 되는 데, 이 버튼을 눌러 콘텐츠 등급을 산정하도록 합니다.

그러면, 주요 국가의 등급 레벨이 나오는데, 특별한 사항이 없다면, 등급 적용 버튼을 눌러, 등급을 적용합니다.

 

7. 가격 및 배포.

구글 플레이 콘솔 왼쪽 메뉴에 가격 및 배포 메뉴를 선택하고, 뜬 창에서, 적절한 항목을 선택하고, 임시 저장 버튼을 누릅니다. 추천 항목은 배포 방법 선택 무료, 국가는 모두 사용 가능, 주로 어린이를 대상으로 앱 항목은 아니오, 광고 포함 유무는 아니오, 콘텐츠 가이드 라인 체크, 미국 수출 법규 체크 입니다.

 

8. 베타 테스트 출시하기.

위의 사항들을 잘 마무리 한다면, 필수 항목에 다음과 같이 모두 녹색으로 체크 될 것 입니다.

그럼 왼쪽 메뉴의 앱 버전 메뉴를 선택하고, 공개 트랙의 관리 버튼을 눌러 줍니다.

다음 창의 출시되지 않은 버전이 있습니다. 항목에서 버전수정 버튼을 눌러 주도록합니다.

앱 버전 창이 뜨면 검토 버튼을 눌러 줍니다.

그럼 언제나 비활성화 상태에 있던 베타 출시 시작 버튼이 활성화 되어 있습니다. 이 버튼을 눌러 베타 출시를 시작하도록 합니다.

자 그럼 아래와 같이 베타 버전이 출시가 완료 되었습니다. 일단 상태는 게시 보류중 상태 입니다. 이 것은, 구글 플레이 측에서, 이 앱이 게시가능한 앱인지 검토를 하고 있는 상태 입니다. 몇 분 혹은 몇 일간 이 상태가 계속 될 수 있습니다.

 

8. 베타 테스트 참여 주소 확인하기.

베타 버전 이라도 출시되면, 왼쪽 메뉴가 기존과 다르게 변경이 됩니다. 이 중 출시 관리 > 앱 버전 메뉴 항목을 선택해 줍니다.

다음 창에서, 테스트 참여 대상 관리 항목의 공개 베타 테스트 를 선택해 줍니다.

그러면, 접혀 있던 항목이 펴지며, 공개 베타 테스트 참여 주소를 확인할 수 있습니다.

위 주소로 접속하면 다음과 같은 테스트 참여 페이지를 볼 수 있습니다.

 

 

 

 

Posted by 창업닉군
,

iOS에 비하면 안드로이드는 출시 과정이 매우 간단한 편 입니다. 안드로이드 스튜디오 기준으로 설명을 하도록 하겠습니다.

1. 키스토어 생성

키스토어는 일종의 도장같은 파일 입니다. 이 것을 통해, 앱의 코드를 봉인 한다고 생각하시면 편하실 것 같습니다.

우선 메뉴를 통해, 코드에 사인하는 메뉴를 선택합니다.

이 후 나오는 창에서는 APK 파일을 선택하도록 합니다. 그리고, next를 선택합니다.

그 다음 키스토어를 선택하는 창이 뜨는 데 우리는 키스토어가 없으므로, 키스토어를 생성하도록 합니다.

그러면 키스토어를 생성하는 창이 호출 됩니다.

키스토어는 APK를 사인할 수 있는 키가 잔뜩 들어 있는 파일 입니다. 앱은 처음에 출시할 때, 사인된 키로만 싸인해 올려야 하는데, 이 키가 여러개 들어 있어 키스토어 입니다. 주의 할 점은 구조상, 키스토어에 여러개의 키를 넣을 수 있지만, 키스토어에 1개의 키만 넣는 것이 좋습니다. 키스토어 하나가 망가지면, 모든 키들을 복구 할 방법이 없을 테니까요. 키스토어에 키스토어 하나가 좋습니다.

아래와 같이 키스토어 생성창이 떴다면, 키스토어 패스워드와 그 패스워드 확인을 입려가혹, 그아래 키 별칭(Alias)에 이키의 용도를 알기쉽게 적어줍니다. 그리고, 이키의 패스워드 와 그 패스워드 확인값을 잘 입력해 줍니다. Validdity는 이 키의 유효한 햇(year)수 입니다. 이 기간이 지나면 사용불가할 것 같은데, 직접 확인해 보지는 못했습니다. 그 외 아래 항목에 적당한 값일 입력하고, OK 버튼을 눌러줍니다.

* 패스워드를 입력할 때, 절대로 한글 상태로 놓고 입력하지 마세요. 한글로 패스워드가 들어 갑니다. (맥에서, 윈도우 에서도 일지 모름).

그럼 키스토어가 생성되고, 자동으로 생성했던 키스토어에서 필요한 정보들이 입력됩니다. 만약 기존의 키스토어를 사용하려면, Choose existing 버튼을 눌러 기존의 키 스토어를 선택하면됩니다. 자동으로 선택되어 있으므로, 바로 next를 눌러 다음 단계로 넘어 갑니다.

결과물 출력 폴더와 빌드를 릴리즈, 디버그 방식 중 선택하는 옵션이 있습니다. debug모드는 해당 파일을 디버깅해 볼 수 있는 파일입니다. 우리는 출시예정이므로, release를 선택해야 합니다. 가장 아래 Signature Versions  라는 선택항목인데, v1을 선택해 줍니다. (v1 빼고 업로드 하면, no jar .... 이라는 오류가 뜹니다.)

그럼 빌드가 시작되고, 완료되면, 메시지 창이 떠 줍니다. 이 창에서 locate 를 선택하면, 빌드된 결과물이 있는 폴더로 이동합니다. (우 하단에 해당 다이얼로그가 표시됩니다.)

이 앱을 스마트폰으로 이동해 실행하면, 앱을 설치할 수 있습니다. (에뮬레이터는 드래그 & 드롭으로 복사 및 설치 가능)

 

 

 

 

 

 

 

 

Posted by 창업닉군
,

푸쉬가 오면 상단 알림 리스트에 해당 메시지가 표시됩니다. 이 기능을 푸쉬가 오지 않아도, 앱에서 넣는 기능입니다.

주의 할 점은 오레오 버전 이상에서는 채널 기능이 있어야 해 코드 분기가 있습니다. 그 외에는 코드가 간단해 이해 하기 쉬우리라 판단됩니다.

1. MainActivity.java onCreate 메소드 속, 버튼 이벤트 추가.

        // 버튼을 표시하도록 합니다.
        Button btn = findViewById(R.id.button);
        btn.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick( View v )
            {
                showNoti1();
            }

        });

 

2. MainActivity 클래스에 알림표시 메소드.

    private static final String CHANNEL_ID = "channel1";
    private static final String CHANNEL_NAME = "Channle1";

    NotificationManager manager;
    
    
    
    
    
    
    
    // 헤드 업 UI를 표시하도록 하는 메소드.
    public void showNoti1()
    {

        manager = ( NotificationManager ) getSystemService(NOTIFICATION_SERVICE);
        NotificationCompat.Builder builder = null;


        // [-- 안드로이드 버전 별로 알림을 호출하는 방법 분리. --]

        // 오레오(O) 버전 이상인 경우, 채널을 지정해 알림을 표시하도록 합니다.
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O )
        {

            // 채널이 없는 경우 채널을 생성하도록 합니다.
            if( manager.getNotificationChannel(CHANNEL_ID) == null )
            {
                manager.createNotificationChannel(new NotificationChannel(
                    CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT
                ));
            }

            builder = new NotificationCompat.Builder(this, CHANNEL_ID);

        }

        // 그 외에는 채널 없이 알림을 표시해 주도록 합니다.
        else{


            builder = new NotificationCompat.Builder(this, CHANNEL_ID);


        }


        Log.d(TAG, "왜 안되나?");


        // 생성된 빌더에 메시지들을 끼워 넣도록 합니다.
        builder.setContentTitle("간단 알림");
        builder.setContentText("알림 메시지 입니다.");
        builder.setSmallIcon(android.R.drawable.ic_menu_view);
        Notification noti = builder.build();

        manager.notify(1, noti);



    }

 

* 버튼은 레이아웃을 통해 추가하고, 이 버튼을 눌렀을 때 알림이 표시되면 성공입니다.

 

 

 

'Android' 카테고리의 다른 글

android 베타 버전으로 앱 등록하기  (3) 2019.04.08
android 출시용 apk 파일 만들기  (0) 2019.04.08
FCM Push notification (android)  (0) 2019.04.06
android webview, javascript 통신  (1) 2019.04.03
android 웹뷰(webview) 설정  (0) 2019.04.02
Posted by 창업닉군
,

서버에서 안드로이드로 메시지를 보내는 방법을 포스팅합니다. 파이어 베이스에서는 쉽다고 홍보하고 있지만, 프로그래머 대부분은 여기서 머리를 싸매게 됩니다. 일단 파이어 베이스 문서(공식)가 별로 자세하지 않고, 설명 띄엄 띄엄하고, 솔루션도 제공하지 않습니다. 따라서 가장 최신판 책을 한 권 구매하시기 바라며, 이 내용은 그저 트러블 해결 용도로 이용하시기 바랍니다.

참고 사항.

참고 사항 1. 메시지를 보낼 때, 앱이 켜져 있으면, 내부 앱에 메시지가 뿌려지고, 앱이 꺼져 있으면, 상단 알림으로, 메시지가 보내저 소리등이 나게 됩니다. (기본적으로.).

참고 사항 2. 설정에서 알림 받지 않기를 해도 앱이 켜져 있을 때는 알림을 받습니다. 꺼져 있을 때만, 받지 않습니다. 그러므로, 이 것을 이용해 챗팅을 구현해도 상관이 없습니다.

참고 사항 3. 알림을 받기로 허용하면, 앱을 한 번 실행한 뒤 부터 알림이 도착하기 시작합니다. (참고해 주세요).

 

1. FCM 에서 프로젝트 만들기.

   1) 계정 만들기. (생략).

     아마 별로 어려울 것이 없을 것이라 생각이 됩니다.

   2) 프로젝트 생성.

      이름을 잘 정해서 하나 만들어 주시기 바랍니다.

 

2. Android 앱 추가.

   1) Android 앱 등록.

      패키지 명을 잘 등록해 줍니다.

 

   2) 구성파일 다운로드.

      설명에도 나와 있지만, 프로젝트 보기로 전환하고, (중요), 다운로드 파일을 google-services.json 파일을 App 폴더 하위에 추가하도록 합니다.

 

   3) Firebase SDK 추가.

      이 부분은 설명은 참 쉽게 되어 있는 데 (추가하는 것 자체는 쉽습니다.). 입력값 그대로 넣으면 빨간 줄이 쭉쭉 그여질 수 있습니다. 구글 SDK가 업데이트 되면, Firebase SDK 업데이트도 다시 되어야 하는데, 파이어 베이스는 이런일에 둔감합니다. 그래서, 버전이 잘 안맞습니다. 그래서 트러블이 생길 수 있는 데, 이것들은 검색을 통해 확인해야 합니다.

설치할 때, 버전은 이 문서를 참고 하도록 합니다. : https://firebase.google.com/docs/android/setup?authuser=0

일단 시키는 대로 추가하고, 우상단 Sync now를 눌렀는데, 아래와 같이 트러블이 생깁니다.

저와 같이 트러블이 생기는 분은 아래와 같이 몇개의 모듈을 추가해 주면 해결됩니다.

implementation 'com.android.support:animated-vector-drawable:28.0.0'
implementation 'com.android.support:support-media-compat:28.0.0'
implementation 'com.android.support:support-v4:28.0.0'

* 그럼 아래와 같이 밑줄이 사라진 것을 볼 수 있습니다.

 

   4) 앱을 실행하여 설치확인

   이 부분은 일단 건너 뛰도록 합니다.

 

3. FCM을 사용할 수 있도록 서비스를 만들어 등록하기.

공식문서 : https://firebase.google.com/docs/cloud-messaging/android/client?authuser=0

FCM을 사용하려면, 해당 모듈을 등록하고, 서비스 클래스가 2개가 필요합니다.

   1) 조금전까지 설치했던, 모듈들은 모두 FCM Core 입니다. 코어는 파이어 베이스를 사용하기 위해 기본적으로 설치되어 있어야하는 것들 이고, 메시지를 사용하려면, app 수준 모듈에 아래와 같이 모듈을 설치 해줍니다.

   implementation 'com.google.firebase:firebase-messaging:17.3.4'

 

   2) MyFirebaseMessaginService.

   실제 메시지를 받는 서비스의 클래스 입니다. 위 이름이 예제 파일과 같은 이름인데, 바꾸지 않는 것이 좋습니다. 파이어 베이스를 몇 번해 보았지만, 바꾸지 않는 것을 권해 드립니다. 서비스를 만들 때, FirebaseMessagingService클래스를 상속해서 만들도록 합니다.

생성자 외 메소드는 모두 지웁니다. 그리고, Ctrl+o를 눌러, onNewToken, onMessageReceived 가상 메소드들을 재정의 하도록 합니다. (선택하면 자동완성됨).

onNewToken은 메시지에 사용될 토큰을 새로 발급받았을 때, 호출되는 메소드로 이 토큰은 각각의 기기를 식별해 사용하는 메소드 입니다. onMessageReceived 는 메시지를 받았을 때 호출되는 메소드 입니다. 아래는 해당 서비스의 소스 코드 입니다.

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

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

public class MyFirebaseMessagingService extends FirebaseMessagingService {

    private static final String TAG = "FCM";

    public MyFirebaseMessagingService() {
    }

    // 새로운 토큰을 확인했을 때 호출되는 메소드.
    @Override
    public void onNewToken(String token) {
        super.onNewToken(token);

        // 토큰 정보를 출력합니다.
        Log.e(TAG, "onNewToken 호출됨: " + token);

    }


    // 새로운 메시지를 받았을 때 호출되는 메소드.
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);

        // 받은 메시지를 출력합니다.
        Log.e( TAG, "onMessageReceived 호출됨" + remoteMessage );

    }
}

 

   3) 서비스 xml등록.

우리가 위에서 서비스로 앱을 등록해 주었기에, 서비스를 xml에 등록할 필요가 없을 것 같지만, 그렇게 해두면 푸쉬 메시지를 받지 못합니다.  아래 코드를 확인해 푸쉬 메시지를 등록할 수 있게 해 줍니다.

    <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">
        <service
            android:name=".MyFirebaseMessagingService"
            android:enabled="true"
            android:exported="true">

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

        </service>

   4) 퍼미션 등록

푸쉬 서비스를 이용하기 위해, internet 퍼미션을 등록합니다.

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

 

4. 화면 레이아웃 만들기.

   이 부분은 적당히 만들어 주시 기 바라며, 굳이 만들 필요가 없을 수도 있습니다.

 

5. MainActivity 에  토큰관련 메소드 추가.

   아래 토큰을 받은 시점과 토큰을 받은 후 원하는 시점에, 토큰ID를 가지고 오는 메소드 입니다. (참조해서 쓰면됩니다.)

        // 토큰이 등록되는 시점에 호출되는 메소드 입니다.
        FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(this,

                new OnSuccessListener<InstanceIdResult>() {
                    @Override
                    public void onSuccess(InstanceIdResult instanceIdResult) {

                        String newToken = instanceIdResult.getToken();
                        Log.d( TAG, "새토큰" + newToken );

                    }
                }

        );



        // 버튼을 눌렀을 경우, 저장된 토큰을 가지고 오는 메소드를 설정합니다.
        Button btn_1 = findViewById(R.id.button);
        btn_1.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {

                String savedToken = FirebaseInstanceId.getInstance().getId();
                Log.d(TAG, "등록되어 있는 토큰ID:" + savedToken);



            }
        });

   

6. 메시지를 받았을 경우 처리하는 메소드 넣기.

   1) MyFirebaseMessagingServiced 서비스에서 메시지를 받은 경우 처리.

    // 새로운 메시지를 받았을 때 호출되는 메소드.
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);

        // 일단 받은 데이터 중, 내용만 가지고와 출력하는 메소드 입니다. (파이어 베이스 홈페이지에서 보내면 데이터는 값이 없을 수 있습니다.)
        String from = remoteMessage.getFrom();
        Log.d(TAG,
        "title:" + remoteMessage.getNotification().getTitle()
        + ", body:" + remoteMessage.getNotification().getBody()
        + ", data:" + remoteMessage.getData()
        );


        // 액티비티 쪽으로 메시지를 전달하는 메소드를 호출합니다.
        sendToActivity(
                getApplicationContext()
                , remoteMessage.getFrom()
                , remoteMessage.getNotification().getTitle()
                , remoteMessage.getNotification().getBody()
                , remoteMessage.getData().toString()
                );

    }



    // Activity 쪽으로 메소드를 전달하는 메소드 입니다.
    private void sendToActivity(Context context, String from, String title, String body, String contents ){


        Intent intent = new Intent(context, MainActivity.class);
        intent.putExtra("from", from);
        intent.putExtra("title", title);
        intent.putExtra("body", body);
        intent.putExtra("contents", contents);


        intent.addFlags(
                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP
        );


        context.startActivity(intent);


    }

   2)  Activity에서 받은 메시지를 처리하는 경우.  (소스에 주석을 넣어 두었으므로, 참조하면되겠다.)

    // 서비스로 부터 인텐트를 받았을 때의 처리.
    @Override
    protected void onNewIntent(Intent intent) {
        println( "onNewIntent 호출됨" );

        // 인텐트를 받은 경우만, 값을 Activity로 전달하도록 합니다.
        if( intent != null )
        {
            processIntent( intent );
        }


        super.onNewIntent(intent);

    }



    // 인텐트를 처리하도록 합니다.
    private void processIntent( Intent intent ){

        String from = intent.getStringExtra("from");
        if( from == null )
        {

            // from 값이 없는 경우, 값을 전달하지 않습니다. (푸쉬 노티 메시지가 아닌것을 판단하고 처리하지 않는듯).
            Log.d( TAG, "보낸 곳이 없습니다." );
            return;

        }


        // 메시지를 받은 것우 처리를 합니다.
        Log.d( TAG, "여기서 메시지 응답 처리를 하면 됩니다." );


    }

 

7. 이렇게 하고, FCM 사이트로가 클라우드 메시지를 보내면, 메시지를 무사히 보낼 수 있습니다. 자세한 사항은 FCM의 클라우드 메시지를 확인해 보시면 되겠습니다.

 

 

 

 

   

Posted by 창업닉군
,

android webview, javascript 통신


1. 브릿지 만들기.

별것은 없다. 실제 안드로이드와 연동동작을 할 클래스들을 따로 모아두었다 생각하면된다. 다만 클래스 중 javascript에서 호출될 메소드는 @JavascriptInterface 어노테이션을 붙혀주면된다. 아래는 예제 소스다. (로그를 적어주는 메소드).

public class AndroidBridge {

private String TAG = "AndroidBridge";

// 로그를 띄우는 메소드 입니다.
@JavascriptInterface
public void call_log( final String _message ){

Log.d(TAG, _message);

}


}


2. 브릿지 셋팅하기.

MainActivity 클래스,  onCreate 메소드 적당한 곳에, 적당히 셋팅해 주면된다. (그냥 클래스를 생성하고, 셋팅하는 것 뿐이다.)

AndroidBridge ab = new AndroidBridge(wv, MainActivity.this );
wv.addJavascriptInterface( ab, "Android" );

셋팅하는 addJavascriptInterface 메소드의 첫 인자는 브릿지 클래스를 바탕으로 생성한 객체, 2번째는 javascript의 어떤 객체속에 인터페이스 메소드들을 넣어줄까를 결정하는 값이다. 아래 호출 부분을 보면 쉽게 이해가 가리라 생각된다.



3. javascript 호출부분.

그냥 호출하면된다. 아래와 같이.

<javascript>

   window.Android.call_log('원하는 메시지');

</javascript>



4. 안드로이드 에서 javascript로 메시지 보내기.

웹의 a 태그 href 에서 javascript를 실행할 때, <a href="javascript:alert('안녕');"> 등의 방법으로 간단히 자바 스크립트를 실행할 수 있을 것이다. 똑 같이  webview.loadUrl("javascript:alert('안녕');") 이라고 전달해 준다면, 똑 같은 동작을 한다.
다만, Webview와 Android는 비동기 상태 이므로 쓰레드로 넣어전달해 주어야 한다.

주의 할점. Handler를 넣으려고 하면, 자동완성 순서상,  import java.util.logging.Handler 가 등록되며, 되지 않을 수 있다. 꼭, import android.os.Handler를 포함시키자.

javascript 속 메소드를 호출하는 코드가 있는, 클래스에 아래와 같이 Handler를 추가해 주도록 합니다.

final public Handler handler = new Handler();

그리고 적당한 곳에 아래와 같이 자바 스크립트 함수를 호출하면됩니다.

handler.post(new Runnable() {
@Override
public void run() {
mAppView.loadUrl("javascript:alert('안녕')");
}
});


5. 전체 소스

>> MainActivity.java


import android.app.Activity;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.WebChromeClient;
import android.webkit.WebView;

public class MainActivity extends AppCompatActivity {

private WebView wv;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Toolbar toolbar = findViewById(R.id.toolbar);
// setSupportActionBar(toolbar);



// 웹뷰 컨트롤러를 가지고 옵니다.
wv=(WebView)findViewById(R.id.activity_main_webview);



// 웹뷰에 자바 스크립트를 사용할 수 있게 허용.
wv.getSettings().setJavaScriptEnabled(true);

// 각종 알림 및 요청을 받게되는 WebViewClient를 설정합니다. (직접만든 웹뷰 클라이언트를 상속한 클래스 설정)
wv.setWebViewClient(new CustomWebViewClient());

// 크롬 핸들러 설정. 이 핸들러는 자바 스크립트 대화상자, favicon, 제목과 진행상황 처리에 사용하기 위해.
// WebChromeClient를 구현한 것 입니다.
wv.setWebChromeClient( new WebChromeClient());



// 웹뷰에 첫 주소를 연결하도록 합니다.
String first_addr = getString(R.string.first_addr);
wv.loadUrl(first_addr);



// FloatingActionButton fab = findViewById(R.id.fab);
// fab.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View view) {
// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
// .setAction("Action", null).show();
// }
// });



// Javascript 브릿지를 사용하도록 합니다.
AndroidBridge ab = new AndroidBridge(wv, MainActivity.this );
wv.addJavascriptInterface( ab, "Android" );


}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);




return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();

//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}

return super.onOptionsItemSelected(item);
}
}




>> AndroidBridge.java


import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;

import android.os.Handler;


public class AndroidBridge {

private String TAG = "AndroidBridge";
final public Handler handler = new Handler();

// 새성시 내부적으로 사용할 코드들을 저장합니다.
private WebView mAppView;
private MainActivity mContext;

// 기본 생성자 입니다.
public AndroidBridge(WebView _mAppView, MainActivity _mContext)
{

// 입력받은 값들을 저장하도록 합니다.
mAppView = _mAppView;
mContext = _mContext;

}

// 로그를 띄우는 메소드 입니다.
@JavascriptInterface
public void call_log( final String _message ){

Log.d(TAG, _message);

handler.post(new Runnable() {
@Override
public void run() {
mAppView.loadUrl("javascript:alert('["+ _message +"] 라고 로그를 남겼습니다.')");
}
});



}


}

>> 웹페이지 파일.

<html>

<head>

<title>일단 웹뷰</title>



</head>



<body>

일단 웹뷰를 표시하도록 합니다.!!!

<a href="javascript:location.reload(true);">새로고침</a>
<a href="javascript:window.Android.call_log('연동테스트');">연동</a>

</body>



</html>




그럼.. 이만..







Posted by 창업닉군
,

웹뷰는 여러가지 이유로 사용되지만, 최근에는 웹앱을 제작하기 위해 많이 사용하게 되는 것 같다.
나도 웹앱을 만들기 위해 웹뷰를 사용할 일이 있어 관련 문서를 찾아 보았는데, 예전보다 많은 부분이 편리해진것 같다.

1. 참고문서.
   안드로이드 웹뷰 띄우기https://pizzaplanet.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%9B%B9%EB%B7%B0-%EB%9D%84%EC%9A%B0%EA%B8%B0

   clearTextTrafficPermitted 속성 (웹접속 권한 관련). : https://nobase-dev.tistory.com/81

   웹뷰 클라이언트 (WebViewClient) - 아래 문서 중 어느 것이 나 봐도 좋다.
   https://baeksupervisor.tistory.com/103   
   https://blog.naver.com/jolangma/150106605897

   웹크롬 클라이언트(.WebChromeClient )로 각종 웹뷰 콜백 받기.
   http://ankyu.entersoft.kr/Lecture/android/webview_03.asp .

   웹뷰에서, window.localStorage 사용하기.
   http://charlie0301.blogspot.com/2015/09/windowlocalstorage-android-webview.html .

 

 

2. 프로젝트를 생성.

   우리는 웹앱을 위한 웹뷰를 사용할 것 이므로, Basic Activity를 사용해, 프로젝트를 생성합니다.

 

3. ui 편집.

   웹앱을 만들 것이므로, 전체 화면에 webview가 꽉찬 형태의 ui가 필요합니다. 그래서 레이아웃의 수정이 필요한데, content_main.xml 파일을 열어 약간의 수정을 해 보도록 하자.

먼저 <TextView
          ~~~~~~~~
          ~~~~~~~~ />
부분을 지우자. 그리고 이부분을 웹뷰로 대체 하도록 하자.
   

<WebView
        android:id="@+id/activity_main_webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="0dp"
        tools:layout_editor_absoluteX="8dp"
        tools:layout_editor_absoluteY="8dp"  />

MainActivity.java 파일에서 툴바 관련 소스를 2개 정도 주석 처리 합니다. (툴바를 호출하는 구문임)

Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolBar);

 

 

4. 권한 부여.

   웹뷰로 인터넷에 접속하려면, 권한을 줘야 하는데, 이 것은 activity_main.xml 파일을 수정함으로써 가능하다.

   <application>~~</application> 바로 아래, 아래와 같이 퍼미션을 주어 인터넷 권한 부여를 하도록 하자. 예전에는 인터넷 권한만 필요했던것 같은데, 지금은 몇가지 권한이 더 요구된다, 이 글을 보시는 분들은 구 버전을 사용해 모든 권한이 필요 없을 수 있으므로, 하나씩 단계별로 권한을 적용해 보도록 하자.

   <!-- 인터넷 권한 부여 (net::ERR_CACHE_MISS) -->
   <uses-permission android:name="android.permission.INTERNET" />

   <!-- 코드상 텍스트를 입력해 접속 가능한 url 입력 가능하게 하기. (net::ERR_CLEARTEXT_NOT_PERMITTED) -->
    <manifest ...>
         ...
         <application
                    ...
                    android:usesCleartextTraffic="true"

 

   이 상황에서 에뮬레이터를 동작해 보면 크게 무리없이 동작을 한다는 것을 알 수 있다.

 

5. 웹페이지 연결.

   MainActivity.java 파일을 열어, 아래와 같이 수정합니다.

   웹뷰 컨트롤을 저장할 변수 선언 (MainActivity 클래스).

   WebView wv;

   onCreate 메소드 속 리턴 바로 위에 아래와 같이 추가하자.

   // 웹뷰 컨트롤러를 가지고 옵니다.
   wv=(WebView)findViewById(R.id.activity_main_webview);

   // 웹뷰에 데이터가 로드 될 수 있도록 합니다.
   wv.loadUrl("http://naver.com");

   *이렇게 한뒤 안드로이드를 실행해 보면, 브라우저에서 창을 띄우게 된다. 그래서 웹뷰 클라이언트를 이용해 웹부안에서 페이지가 표시되도록 웹뷰 클라이언트 객체를 사용해야 한다. 다음 단원을 참고해 웹뷰 클라이언트를 셋팅하도록 하자.

 

6. 웹뷰 클라이언트 클래스 생성.

   MainActivity.java 파일이 있는 패키지에, WebViewClient 클래스를 상속한 클래스를 만들어 추가하도록 합니다.

   

   그리고, 몇줄을 Activity에 몇 줄의 설정을 더 추가합니다. 붉은 색이 추가된 것들

   // 웹뷰 컨트롤러를 가지고 옵니다.
   wv=(WebView)findViewById(R.id.activity_main_webview);


   // 웹뷰에 자바 스크립트를 사용할 수 있게 허용.
   wv.getSettings().setJavaScriptEnabled(true);

   // 각종 알림 및 요청을 받게되는 WebViewClient를 설정합니다. (직접만든 웹뷰 클라이언트를 상속한 클래스 설정)
   wv.setWebViewClient(new CustomWebViewClient());


   // 웹뷰에 첫 주소를 연결하도록 합니다.
   wv.loadUrl("http://naver.com");

   * 여기까지 하고 웹뷰를 실행해 보면, 아래와 같이 페이지가 호출됩니다.

 

7. 툴바 제거.

   우리가 원하는 것은 전체 화면의 웹뷰 입니다. 그래야, 웹으로 전체를 다 꾸밀 수 있고, iOS에서 역시 웹소스를 그대로 사용할 수 있습니다. 그럼 툴바를 제거해 보도록 하겠습니다.

   1) activity_main.xml 파일 속, andrioid.support.design.widget.AppBarLayout 태그를 <!-- --> 를 이용해 주석 처리해 주도록 합니다.

   2) MainActivity 파일 속, 앱바 및 툴바 관련 코드를 모두 주석 처리를 해 줍니다.

       Toolbar toolbar = findViewByID(R.id.toolbar);
       setSupportActionBar(toolbar);

 

 

8. 플로팅 액션 버튼 제거.

  여기까지 작업된 화면을 보면, 전체적으로 괜찮지만, 오른쪽 하단에, 플로팅 액션 버튼이 있다. 물론 사용을 하면 편하지만, 나는 웹앱을 만들 목적 이므로, iOS와 함께 사용하려면, 웹으로 다 구현해야 한다. (그래야, iOS에서도 같은 표시가 될 것 이므로).

   activity_main.xml 파일속 <android.support.design.widget.FloatingActionButton> 태그를 지워 줍니다.

그리고, 아래의 플로팅 액션 버튼 관련 코드를 주석 처리 해 주도록 합니다.

   FloatingActionButton fab = findViewById(R.id.fab);
   fab.setOnClickListener((view)->{
      Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
         . setAction("Action", null).show();
   });

 

여기까지 되었다면, 웹앱을 작업하는 데 무난 할 것 이다.

 

9. 추가.

각종 콜백을 받으려면, 웹뷰 크롬을 설정해 주어야 합니다. 지금은 예시로 아래 처럼 해 두지만, WebChromeClient를 상속한 서브클래스를 만들어 두면, 웹뷰의 다운로드 프로그래스등의 이벤트를 받을 수 있습니다. (상단의 웹크롬 클라이언트(.WebChromeClient )로 각종 웹뷰 콜백 받기 문서참조). 그리고 WebChromeClient를 설정해 주지 않는다면, Alert이 뜨지 않습니다. 참고해 주세요.

wv.setWebViewClient(new CustomWebViewClient());
wv.setWebChromeClient( new WebChromeClient());

 

10 추가, 웹뷰에서 window.localStorage 사용하기.

당연한 이야기 겠지만, 파일 읽고 쓰는 퍼미션이 있어야 할 것 이다.

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

 

그리고 아래는 안드로이드에 추가해야할 소스

WebSettings settings = webview.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setDatabaseEnabled(true);

File dir = getCacheDir();
if (!dir.exists()) {
    dir.mkdirs();
}
settings.setAppCachePath(dir.getPath());
settings.setAppCacheEnabled(true);

 

 

* 혼동하지 말 것.

   안드로이드  에뮬레이터 에서 http://localhost 로 주소를 주면, 당연히 오류가 난다. 가상이지만, 하나의 피씨로 ip를 할당 받아 사용한다. 그러므로, http://localhost 해 봐야 연결되지 않는다. 당연한 이야기 같지만, 잘 혼동하므로, 주의 하자.

   아래는 각 운영 체제별 커맨드 명령.

   MAC : ifconfig
   Windows : ipconfig

   미묘(?)한 차이가 있으므로, 잘 구분해서 IP 를 확인하도록 하자.

 

 

 

 

Posted by 창업닉군
,

선행 작업.
파이어 베이스 프로젝트 생성 : http://nicgoon.tistory.com/171
안드로이드 프로젝트 생성 및 안드로이드 프로젝트에 파이어 베이스 추가 : http://nicgoon.tistory.com/172

 문서를 참고 하시기 바랍니다.

 

 

위 선행작업이 완료되었다면, 파이어 베이스로(https://console.firebase.google.com) 접속 합니다. 아래와 같은 창이 뜨면 자신이 설정할 프로젝트를 선택해 줍니다.

그럼 프로젝트 창이 뜨는 데, 왼쪽 아래의 Notification 항목을 선택해 줍니다.

그럼 아래와 같은 창을 만납니다. 일단 이 곳에서는 자신의 앱이 설치된 스마트폰에 메시지를 보낼 수 있습니다. 테스트 과정에 사용해 보기 좋으며, 이를 참고해서 추후 웹서버를 구성하면 좋습니다. 일단 클라이언트 부분을 완성해야 하므로, 아래에 표시된 문서 보기를 눌러 줍니다.

아래 화면이 나오면 조금 더 내려서, [ANDROID 설정] 버튼을 눌러줍니다.
바로 가기 주소 : https://firebase.google.com/docs/cloud-messaging/android/client

 

-> [firebase message service] 플러그인 추가.
앱 수준의 [Build.gradle] 에 플러그인을 추가하면, 되며, 차 후 다른 서비스의 플러그인을 추가할 때도, 이 곳에 플러그인을 추가하면 됩니다.

 

 

-> 메시지를 수신하고 처리하는 서비스와, 토큰을 관리하는 서비스를 추가한다.
    액티비티 아래 부분에 추가하는 것이 가장 적당하다.

 

-> 옵션 사항으로, 메시지가 왔을 때 메시지 리스트에 쌓을 스택 바에 표시될 아이콘에 관한 설정을 할 수 있다.
<Application> 태그 사이에 추가하고, 아이콘을 설정한 메타 태그 속 [android:resource=@] 값은 이미지 값을 설정해 둔다.

-> 토큰을 관리하는 [FirebaseInstanceIdService] 파일을 생성하고, 필요한 코드를 추가합니다.
이는 설명을 잘 읽어 보고, 아래 내용의 파일을 액티비티가 있는 폴더에 그대로 생성합니다.

https://github.com/firebase/quickstart-android/blob/master/messaging/app/src/main/java/com/google/firebase/quickstart/fcm/MyFirebaseInstanceIDService.java#L35-L45

 

-> [MyFirebaseMessagingService.java] 파일 추가.
해당 문서에는 나와 있지 않지만, 위의 매니페스트에서 서비스가 실행됨을 정의했으므로, 당연히 파일을 추가해야 한다.
아래의 파일을 액티비티가 있는 폴더에 생성해 추가한다.

https://github.com/firebase/quickstart-android/blob/master/messaging/app/src/main/java/com/google/firebase/quickstart/fcm/MyFirebaseMessagingService.java

 

이 파일을 추가 후 아래 표시될 아이콘을 변경해 주어야 한다.

 

아래 이미지 처럼 /App 폴더 속 [Build.gradle] 파일에 [compile 'com.firebase:firebase-jobdispatcher:0.6.0']을
추가해 줍니다.

 

마지막으로, 메인 액티비티가 있는 폴더에, 아래 파일을 추가해 주고, 실행하면됩니다.

https://github.com/firebase/quickstart-android/blob/master/messaging/app/src/main/java/com/google/firebase/quickstart/fcm/MyJobService.java

 

-> 파이어 베이스 콘솔을 통해 메시지를 보냅니다.

여기까지하고 앱을 실행시키고, 메시지를 보내면, 메시지가 오는 것을 확인할 수 없습니다.
다만 Android Monitor에 찍힌 값을 보면 메시지가 왔을 때 본문을 확인할 수 있습니다.
( Android Monitor에서 [onMessageReceived]값을 검색어로 입력 하면 확인 가능.  )

 

-> 메시지 푸쉬 부분을 구현하기 위해, [MyFirebaseMessagingService.java] 파일의 [onMessageReceived]
파일의 코드를 수정합니다.

그리고 메시지를 보내면, 메시지가 오는 것을 확인할 수 있습니다.

'Android > FCM 연동' 카테고리의 다른 글

안드로이드 FCM push Message (2/3)  (0) 2017.08.27
안드로이드 FCM push Message (1/3)  (0) 2017.08.27
Posted by 창업닉군
,

선행되어야할 작업.

-> 파이어 베이스 프로젝트 생성 : http://nicgoon.tistory.com/171

 

이 포스트에서는 안드로이드 앱에 FCM을 추가하는 하는 작업을 합니다.
공식 문서( https://firebase.google.com/docs/android/setup )를 열어 함께 보며 작업 하시기 바랍니다.

 

먼저 [google-service.json] 파일을 다운 받습니다. 방법은 파이어 베이스에 로그인하고, 원하는 프로젝트를 선택합니다.
그리고, 아래 그림처럼 [프로젝트 설정]을 선택합니다.

아래와 같은 페이지가 나오면, [google-services.json] 파일을 다운 받은 후 잘 저장해 둡니다.

일단, 안드로이드 프로젝트를 생성하되 아무것도 없는 빈 문서를 생성합니다. (굳이 복잡할 필요가 없으므로....)

다운로드 받은 파일을 생성한 안드로이드 프로젝트에 [App/] 폴더 아래에 추가해 줍니다.

 

그리고, 코드들을 약간수 정해 주어야 하는데, 메인 액티비티 프로젝트 속에 있는 [Build.gradle] 파일을 열어,
classpath 'com.google.gms:google-services:3.1.0' 문자열을 추가합니다.

 

 

 [App] 폴더 속 [Build.gradle] 파일에는 아래와 같이 2줄을 추가한다.

 

 

 

마지막으로 안드로이드 스튜디오 오른쪽 상단의 [Sync Now] 를 클릭하면 모든 작업이 완료 됩니다. (아래 그림 참조).

'Android > FCM 연동' 카테고리의 다른 글

안드로이드 FCM push Message (3/3)  (0) 2017.08.28
안드로이드 FCM push Message (1/3)  (0) 2017.08.27
Posted by 창업닉군
,

안드로이드에 push message를 보내려면, 구글에 로그인하고, firebase(https://console.firebase.google.com/)에 접속합니다. 그럼 아래와 같은 창을 만나게 됩니다.

 

일단 자신의 앱에 사용할 서비스를 설정할 프로젝트를 만들어 준다. 당연히 앱 이름과 비슷하거나, 같다면, 관리하기 편하다. 화면 하단의 [프로젝트 추가](큰 파란 버튼) 버튼을 누릅니다.

그럼 아래와 같이 프로젝트 만들기 창이 뜹니다. [프로젝트 이름] 항목과 [국가/지역] 항목을 선택한 후 [프로젝트 만들기] 버튼을 눌러 줍니다.

프로젝트가 생성되어 프로젝트 창이 뜨면 아래와 같은 창이 뜹니다.

우리가 할 작업은 안드로이드에 Push를 추가하는 것 이므로, [Android 앱에 Firebase 추가] 이미지를 선택해 줍니다.

아래 이미지 참조

프로젝트 생성 창이 뜨면, Android 패키지 이름을 적으면 오른쪽 아래의 앱 등록 버튼이 활성화되는 데, 이 버튼을 눌러 다음 페이지로 넘어 갑니다. (나머지는 선택사항으로 테스트용으로만 사용할 경우 필요 없습니다.

 

아래 페이지는 구글 서비스 설정파일을 등록하는 것 인데, 아직 안드로이드 프로젝트를 만들지 않았다면, 필요할 때 언제든지 확인이 가능하므로, 오른쪽 아래의 [계속] 버튼을 눌러 다음으로 넘어 갑니다.

마지막으로 파이어 베이스 플러그인을 사용하기 위해, Gradle을 추가하는 설정을 방법을 알려 주는 페이지 입니다.
이 후에 확인이 가능 하므로, [완료] 버튼을 눌러 다음으로 넘어 가도록 합니다.

 

 

 

'Android > FCM 연동' 카테고리의 다른 글

안드로이드 FCM push Message (3/3)  (0) 2017.08.28
안드로이드 FCM push Message (2/3)  (0) 2017.08.27
Posted by 창업닉군
,