레이블이 android인 게시물을 표시합니다. 모든 게시물 표시
레이블이 android인 게시물을 표시합니다. 모든 게시물 표시

2020년 4월 20일 월요일

생체 인증을 위한 CheckboxPreference 만들기

개인적으로 만드는 app에서 생체 인증을 사용하고자 하여 
생체인증 enable/disable을 위한 CheckboxPreference로 옵션을 만들고 
옵션을 변경할 때마다 생체 인증을 하도록 간략히 만들어 봄

사실 너무 간단한 내용이라 안드로이드 가이드만 봐도 충분하다. 


내가 구현하고자 했던 것은
- 생체 인증을 enable/disable 하는 옵션
- 생체 인증을 통해서만 옵션을 enable<->disable 할 수 있음
- 생체 인증 실패 시에는 옵션 변경 없고 에러 발생 시 토스트 표시


안드로이드 생체인증 가이드를 기초하여 구현을 간략히 설명하면

1. 먼저 생체 인증을 위한 jetpack 라이브러리를 사용하고자 종속성을 추가한다.
 : 현재는 1.0.1 이다. 항상 Jetpack 라이브러리의 최신 버전을 확인해야 할 필요 있음.
dependencies {
        implementation 'androidx.biometric:biometric:1.0.1'
    }

2. app 설정에서 CheckboxPreference 추가

<CheckBoxPreference app:iconSpaceReserved="false"
 android:defaultValue="false"  
 android:key="pref_enableBiometricSignIn"   android:summary="@string/settings_summary_entry_enable_biometric_sign_in" 
 android:title="@string/settings_entry_enable_biometric_sign_in" />


3. CheckboxPreference의 onPreferenceChangeListener에서 BiometricPrompt 표시

val biometricCheckBox = findPreference("pref_enableBiometricSignIn") as CheckBoxPreference biometricCheckBox.onPreferenceChangeListener = OnPreferenceChangeListener { preference, newValue ->

 val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Biometric login for app")
    .setSubtitle("Log in using your biometric credential")
    .setNegativeButtonText("Cancel")
    .build()
 val toBe = newValue as Boolean
 val biometricPrompt = BiometricPrompt(this, Executors.newSingleThreadExecutor(),
  object: BiometricPrompt.AuthenticationCallback() {
 
  override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
    super.onAuthenticationError(errorCode, errString) activity ? .runOnUiThread {
     Toast.makeText(activity ? .applicationContext, errString, Toast.LENGTH_SHORT).show()
    }
   }

   override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
    super.onAuthenticationSucceeded(result) activity ? .runOnUiThread {
     (preference as CheckBoxPreference).isChecked = toBe
    }
   }
  }) 

 biometricPrompt.authenticate(promptInfo) 
 false
}


안드로이드 가이드 코드를 거의 그대로 사용하였고 원하는 기능을 위해 아래 사항을 살짝 수정함

- 생체 인증에 성공 시에만 옵션의 check status를 변경하고자 OnPreferenceChangeListener 구현
 : 생체 인증 시에만 check status를 변경하고자 OnPreferenceChangeListener의 리턴값을 false로 전달
 : 생체인증 성공시에만 isChecked 값을 OnPreferenceChangeListener의 argument인 newValue로 설정

- 사용자가 옵션을 변경할 때마다 생체 인증 창을 띄우기 위해 Executors.newSingleThreadExecutor() 로 Executor 매번 생성
 : 솔직히 이게 맞는 것인지는 모르겠음 ㅜㅜ

- 생체 인증 중 지문이 틀릴 때마다 호출되는 onAuthenticationFailed() 를 override하지 않고 onAuthenticationError() 만 override하여 사용자가 Cancel 하였을 때만 처리함 
 : 사용자가 생체 인증을 여러 번 틀려 block될 경우 BiometricPrompt에 안내가 표시되고 OnAuthenticationError()가 호출되기만 하고 사용자가 직접 cancel을 눌러야함.
 : block된 상황에서는 BiometricPrompt를 닫고 다시 옵션을 누르더라도 생체 인증창이 뜨지 않고 OnAuthenticationFailed()가 호출됨. 

2019년 2월 16일 토요일

Google Play Store 업로드를 위해 앱 서명(signing)을 수동으로 하는 방법

최근 구글 정책 변경으로 오랜만에 앱을 업데이트하게 되었음.
https://www.bloter.net/archives/328897

거의 손 놓고 있었던 앱이었는데 기한까지 기능을 제거하지 않으면 앱이 제거된다고 하여
내가 사용하기 위해 어쩔 수 없이 업데이트해서 올릴 수밖에 없었음.
앱을 유지만 하는 것도 생각보다 어렵다.


Play Store에 올리기 위해 Android Studio에서 앱을 서명하려고 했는데
계속 password verification이 실패했다는 메세지가 나와서 결국 command line에서 수동으로 앱을 서명해서 올렸고 현상의 원인은 파악하지 못했음.

일단 앱을 서명하는 방법과 command line에서 수동으로 서명하는 방법은 다음과 같다.

앱을 서명하는 방법

: https://developer.android.com/studio/publish/app-signing#sign-apk

위 가이드를 보고 따라 하면 되는데 간략히 설명하면
Android Studio > Build > Generate Signed Bundle / APK 메뉴를 선택한다.

아래 화면에서 자신의 상황에 맞춰 Create new나 Choose existing을 선택하면 된다.

만약 서명을 위한 key를 새롭게 만든다면 다음을 참고해서 Play Store에 등록하고 업로드용 key로 만들어 등록하여 사용해야 한다.

기존에 만들어둔 key store를 선택하고 password를 입력하면 Key alias에서 key를 선택할 수 있고 이후 key의 password를 입력하면 된다. 이때 key store password가 틀리면 key alias를 선택할 수 없다. key password가 다르다면 서명이 실패한다.


다음 화면에서 release를 선택하면 앱 폴더의 build > output > apk > release 폴더에서 서명된 apk를 찾을 수 있다.


Key Store, 내부 Key 확인 방법

https://docs.oracle.com/cd/E19226-01/821-0027/gfjwc/index.html

위처럼 Android Studio에서 key store 와 key를 확인할 수 있지만 다음 방법으로 확인할 수 있고 key 추출도 가능하다.

- key Store 내부 key 확인 방법
> keytool  -list  -keystore {keystore파일}  -v
   Enter keystore password:

keytool은 JDK 설치 위치에 있으니 아래 글을 참고하세요.
https://stackoverflow.com/questions/5488339/how-can-i-find-and-run-the-keytool

위 명령어를 실행 한 뒤 지정된 key store의 password를 입력하여 keystore의 정보와 내부 key 목록을 확인할 수 있다.

D:\key>"c:\Program Files\Java\jdk1.8.0_112\bin\keytool.exe" -list -keystore key_android.jks -v
키 저장소 비밀번호 입력:

키 저장소 유형: JKS
키 저장소 제공자: SUN

키 저장소에 1개의 항목이 포함되어 있습니다.

별칭 이름: csk
생성 날짜: 2016. 3. 13
항목 유형: PrivateKeyEntry
인증서 체인 길이: 1
인증서[1]:
소유자: CN=csk
발행자: CN=csk
일련 번호: 312a2964
적합한 시작 날짜: Sun Mar 13 19:21:02 KST 2016, 종료 날짜: Thu Mar 07 19:21:02 KST 2041
인증서 지문:
         MD5: ...(생략)
         SHA1: ...(생략)
         SHA256: ...(생략)
         서명 알고리즘 이름: SHA256withRSA
         버전: 3

확장:
... (생략)

- Key Store에서 Key도 추출 가능하다.
> keytool  -export  -keystore {keystore 파일} -alias {key alias이름}  -file {추출할 key 파일명}
   Enter keystore password:
 ex) keytool  -export  -keystore keystore.jks -alias csk -file csk.cer



Command line에서 수동으로 앱 서명 방법
https://developer.android.com/studio/publish/app-signing#signing-manually

Android Studio에서 알아서 해주시면 부득이 한 경우 다음과 같이 진행하면 된다.

1. 빌드
2. zipalign을 사용하여 APK 정렬
3. apksigner를 사용하여 서명


1. 빌드
  Android Studio > View > Tool Windows > Terminal 에서

  gradlew assembleRelease  실행

  그럼 project_name/module_name/build/outputs/apk/에 module_name-unsigned.apk라는 이름의 APK가 생성된다.

2. zipalign을 사용하여 APK 정렬
  zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk

  참고로 zipalign은 Android SDK의 build tool에 있다.
  https://stackoverflow.com/questions/40004884/cant-find-apksigner-executable-to-manually-sign-apk

3. apksigner를 사용하여 서명
   apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk

2018년 2월 1일 목요일

Xamarin.Forms app을 Google Play store에 올리기

초간단 Xamarin.forms app을 만들어 Google Play store에 올림.

https://github.com/hallower/WhooingNewsReader
https://play.google.com/store/apps/details?id=kr.blogspot.charlie0301.NewsReader

리스트뷰만 있는 앱이다 보니 생각보다 Android에서 돌아가는게 느리진 않고
C# 으로 만들앱이다 보니 Google Play store에 올리는데 문제가 있진 않을까 했는데 생각보다 잘 올라가서 신기함.

일단 Google Play store에 올리는 방법
https://developer.xamarin.com/guides/android/deployment,_testing,_and_metrics/publishing_an_application/

Apple App Store에 올리는 방법
https://developer.xamarin.com/guides/ios/deployment,_testing,_and_metrics/app_distribution/app-store-distribution/publishing_to_the_app_store/


Google Play Store에 올리는 방법은 다음과 같이 나눠져 있고
사실 간단한 앱은 Part 1-3까지만 진행하면 등록이 된다.


위 Part 1,2,3를 따라하면서 큰 문제점은 없었고
워낙 잘 정리가 되어 있어서 따라하기만 하면 되지만!

- 모든 절차를 따라해야 나중에 APK를 업로드할 때 문제가 없다.
 : APK 업로드 시 문제점이 무엇인지 알려주므로 문제점이 있다면 Guide를 참고하여 수정 후 다시 수행하면 된다.

- 처음 업로드, 앱 등록 시 Visual Studio에서 자동 처리가 되지 않는다.
 : 처음에는 수동으로 APK를 signing한 뒤 play store에 업로드 해야 한다.

- APK 업로드 시 Signing된 APK를 업로드 해야 한다.
 : 아래 예시 그림에서 signed-apks 폴더 내의 APK를 업로드 해야 한다. 아래 그림에 보이는 MyApp.MyApp.apk는 아니다.
 : 당연한 거겠지만 signing 할 때는 Google Play에 등록된 upload 인증서를 사용해서 signing해야 한다.
  . 참고 http://charlie0301.blogspot.kr/2018/01/google-play-app-signing.html


2018년 1월 27일 토요일

앱 서명 키 관리 Google Play App Signing

앱 signing 인증서를 잃어 버려 동일한 앱을 다시 출시 했던 경험이 있어서
이후 부터는 인증서를 위험하더라도 cloud 서비스에 저장하고 있다.

얼마전 Google Play App Signing가 생긴것을 알게 되어
기존앱의 signing private인증서와 업로드를 위한 인증서를 등록함.



앱 서명 인증서와 앱 업로드를 위한 인증서가 분리되었고
사용자는 업로드를 위한 인증서만을 사용하여 Google Play에 업로드하고
Google Play에서는 업로드 인증서로 업로드된 앱을
다시 앱 서명용 인증서로 서명하여 배포하는 것으로 보인다.



그래서 사용자가 서명용 인증서를 잃어버릴 가능성도 없고
업로드를 위해 사용하는 인증서를 잃어 버려도 
다시 업로드용 인증서를 재등록하여 앱배포에는 문제가 없게 되었음.

진작에 이렇게 하지...

2017년 10월 31일 화요일

Android IAP 대충 정리

안드로이드 IAP가 궁금해서 대충 요약함.

https://developer.android.com/google/play/billing/billing_subscriptions.html

[인앱구독]
인앱 결재를 사용하여 구독 서비스 판매 가능.

- 다양한 주기의 구독 결재 판매 가능
- 월간, 연간 구독을 위해 체험 기간 구성 가능
- Google Play Developer API나 Developer Console을 사용하여 구독 관리
- 사용자는 앱내에서 구독 구매
- 구독 갱신, 업그레이드, 다운그레이드 가능
- 구독 결재 연기 가능

구독은 개발자가 지정한 간격으로 자동 반복되는 결재
인앱 상품과 유사한 절차로 구독을 구성하고 게시함. 단 체험기간과 결재 주기 선택이 다름

[구독 아이템 구성]

  • 구매 유형: 항상 Subscription으로 설정
  • 구독 ID: 구독 식별자
  • 게시 상태: 게시 안 됨/게시됨
  • 언어: 구독을 표시하기 위한 기본 언어
  • 제목: 구독 상품의 제목
  • 설명: 사용자에게 구독에 대해 설명하는 세부정보
  • 가격: 반복 구매 시마다 적용되는 기본 구독 가격
  • 반복 구매: 반복 구매 결제 주기
  • 추가적인 통화 가격(자동 채우기 가능)

[구독 가격]
- 사용 가능 통화로 구독 가격 설정.
- 구독 가격은 0원 초과
- 구독 가격을 다르게 설정 가능(ex 월간 요금 vs 할인된 연간 요금)

[사용자 결제]
- 주간, 월간, 3개월 6개월, 연간, 시즌

[수동 갱신]
- 구독 중 추가 구독 시 만료일이 추가 구독 기간을 포함하여 연장 됨.

[구독 업그레이드/다운그레이드]
- 구독을 변경할 경우 기존 구독이 취소되고 새 구독이 생성됨.

[결제 유예]
- 유예기간 동안 컨텐트 구독 및 사용 권한을 가지지만 요금이 처구되지 않음.
- 단 유예할 수 있는 최장기간은 API 호출 당 1년 이고 1년 중 다시 호출하여 유예기간 연장 가능

[무료 평가판]
- 무료 체험 기간을 설정할 수 있음.
- 개발자가 지정한 기간 동안만 체험이 가능하고 기간이 지나면 구독 결재 주기와 가격에 따라 관리되는 구독으로 자동 전환됨.
- 일반적인 구매 절차를 따라 구매하지만 무료 체험기간 동안은 요금이 0.00 임.

[구독 취소]
- App이 아닌 Play Store의 앱의 My Apps 화면에서 구독에 대한 상태 확인, 취소 가능.
- 구독 취소 시 Google Play는 결재 주기에 대해서는 환불하지 않고 결제 주기 종료까지 구독에 액세스 할 수 있음.

[앱 제거]
- 사용자가 구매한 구독을 포함한 앱 제거시 구독 정보를 알려줌.
- App을 제거하여도 구독은 제거되지 않음.

[결제 처리 및 정책]
- Google Payments를 통해서만 판매 가능
- 구독 상품 구매 시 Transaction 요금은 구매와 같이 30%임.
- 매 구독 마다 구매 정보(판매자 주문 번호와 각각 반복 transaction 정보를 제공함)

[Google Play Developer API]
  • 언제든지 특정 구독의 유효성을 원격으로 쿼리
  • 구독 취소
  • 구독의 다음 결제일 유예
  • 구독을 취소하지 않고 구독 결제 대금 환불
  • 구독 환불 및 취소

https://developer.android.com/google/play/billing/billing_integrate.html#Subs

구독 구현

구독에 대한 구매 흐름을 시작하는 것은 상품에 대한 구매 흐름을 시작하는 것과 비슷하지만, 상품 유형을 "subs"로 설정해야 한다는 점이 다릅니다. 인앱 상품의 경우에서와 정확히 똑같이, 액티비티의 onActivityResult 메서드로 구매 결과가 전달됩니다.
Bundle bundle = mService.getBuyIntent(3, "com.example.myapp",
   MY_SKU, "subs", developerPayload);
PendingIntent pendingIntent = bundle.getParcelable(RESPONSE_BUY_INTENT);
if (bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) {
   // Start purchase flow (this brings up the Google Play UI).
   // Result will be delivered through onActivityResult().
   startIntentSenderForResult(pendingIntent, RC_BUY, new Intent(),
       Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
}
활성 구독을 쿼리하려면 이때도 상품 유형 매개변수를 "subs"로 설정하여 getPurchases 메서드를 사용하세요.
Bundle activeSubs = mService.getPurchases(3, "com.example.myapp",
                   "subs", continueToken);
이렇게 호출하면 사용자가 소유한 모든 활성 구독을 포함한 Bundle이 반환됩니다. 구독이 만료되고 갱신하지 않으면 반환되는 Bundle에 더 이상 구독이 표시되지 않습니다.

[인앱 프로모션]
https://developer.android.com/google/play/billing/billing_promotions.html?hl=ko

- 모든 프로모션 코드는 특정한 상품ID(SKU)와 연결됨.
- 프로모션 코드를 입력하면 특정 아이템의 가격을 완불한 것과 같이 처리됨.

[프로모션 코드 생성 및 사용]
- 프로모션 코드는 Google Play Developer console에서 생성
- 각 프로모션 코드는 Developer console의 단일 상품과 연결
- 프로모션 코드는 일반 인앱 결제와 동일한 절차를 거치되 단 돈대신 프로모션 코드를 사용한다는 점이 다름.
- Google Play 스토어 앱에서 코드를 사용할 수 있음.


[앱에서 프로모션 코드 지원]
- 앱이 다시 시작(onResume())될 때마다 getPurchases() 메서드 호출 후 구매내역 확인
- 앱이 IAP를 지원한다면 구매와 유사하게 진행 되므로 별도 처리 필요 없음.
- 앱 실행 중에 Play 스토어 앱에서 프로모션 코드를 사용하는 경우 처리가 필요함.
: com.android.vending.billing.PURCHASES_UPDATED intent 수신을 위한 처리 필요.

(등록)
IntentFilter promoFilter =
    new IntentFilter("com.android.vending.billing.PURCHASES_UPDATED");
registerReceiver(myPromoReceiver, promoFilter);

(취소)
unRegisterReceiver(myPromoReceiver);

[보안 및 디자인]
https://developer.android.com/google/play/billing/billing_best_practices.html?hl=ko

- 잠금 해제된 콘텐츠를 apk 파일에 포함하여 재배포 할 수 있게 하지 말라.
- 코드 난독 처리 필요, proguard 실행, 아래 방법도 사용가능
 . 메서드를 다른 메서드로 인라인 처리합니다.
 . 문자열을 상수로 정의하는 대신 즉석에서 생성합니다.
 . Java 리플렉션(reflection)을 사용하여 메서드를 호출합니다.

- 보안 랜덤 Nonce를 사용하라.
- 잠금 해제된 콘텐츠를 사용할 때 마다 취소 여부를 확인하라.
- Google Play 공개키를 안전하게 보관하라.
 . 문자열로 삽입하지 말고 런타임에 문자열 생성이나 비트조작을 사용하여 숨겨라.

2017년 10월 30일 월요일

Android IAP references, 개요, 아이템 구매

Android IAP가 궁금해서 대충 요약 해 봄.
: https://developer.android.com/google/play/billing/index.html?hl=ko

대상 : 표준 인앱 상품(일회성 결제), 구독(반복 자동 결제)
- IAP를 사용하여 아이템 판매 시 Google Play가 모든 결제 정보를 처리하여 App은 결재 transaction 관련 처리가 필요 없음.
- Google Play에서 제공하는 결재 서비스를 사용하므로 모든 app들에 대해 일관적인 경험 제공
- Google Play를 통해 세기되는 App 대상



(개요)

[In-app Billing API]
[Google Play Developer Console]

: https://developer.android.com/google/play/billing/billing_overview.html?hl=ko#api

- Google Play 앱에 의해 노출 되는 API를 통해 결제 서비스 사용

- Google Play 앱이 App<->Google Play server 사이 결제 요청, 응답 전달
- App<->Google Play 앱은 IPC를 사용하여 결제 요청, 응답을 받음.
- 결재는 Google Play 서버를 통해서 진행하므로 Google Play app이 서버와 통신할 수 있어야 함.
- Android In-app billing version은 현재 v3가 최신 버전임.
 . App은 google Play의 API를 통해 상품 세부 정보 요청, 상품 주문, 소유 상품 복원 가능.
 . API는 구매 완료 시 주문 정보를 동기적으로 기기로 전파.
 . 모든 구매가 Google Play를 통해 관리됨. 
 . 구매한 상품은 소비 가능하며 소비 시 소유하지 않은 상태로 변경됨.
 . 구독 서비스를 지원함.

- IAP 사용 App을 게시하고 IAP로 판매할 상품을 관리(구매/구독, 상품 목록 생성)함.

- 제품에 대한 다음 정보들을 정의함.
 . 상품 ID(SKU), 상품 유형, 가격, 설명, 구매에 대한 처리 및 추적?

https://developer.android.com/google/play/billing/billing_reference.html?hl=ko#getSkuDetails

표 2. getSkuDetails 요청에서 반환되는 상품 아이템 세부정보를 포함한 JSON 필드에 대한 설명.
설명
productId상품의 ID입니다.
type이 키의 값은 인앱 상품의 경우 “inapp”, 구독의 경우 "subs"여야 합니다.
price아이템의 형식 지정된 가격(통화 기호 포함)으로, 세금을 제외한 가격입니다.
price_amount_micros마이크로 단위의 가격으로, 1,000,000 마이크로 단위가 1 통화 단위와 같습니다. 예를 들어, price가 "€7.99"이면 price_amount_micros는 "7990000"입니다. 이 값은 특정 통화에 대해 현지화된 반올림 가격을 나타냅니다.
price_currency_codeprice에 대한 ISO 4217 통화 코드입니다. 예를 들어, price가 영국 파운드 단위로 지정되어 있는 경우 price_currency_code는 "GBP"입니다.
title상품의 제목입니다.
description상품에 대한 설명입니다.

[Google Play 구매 흐름]

1. App >- IAP 상품 결재 요청 -> Google Play
2. Google Play 결재 양식 요청, 유효성 검사, 구매 transaction 처리
3. Google Play >- 구매 세부 정보(주문 번호, 날짜, 시간, 가격 등) -> Google Play


(In-app Billing API)

https://developer.android.com/google/play/billing/api.html?hl=ko#producttypes
: https://developer.android.com/google/play/billing/billing_integrate.html?hl=ko
  * 구현방법이 좀 다름(확인 필요). https://developer.android.com/training/in-app-billing/preparing-iab-app.html?hl=ko

[아이템 구매]

구매 물품 확인
Bundle ownedItems = mService.getPurchases(3, getPackageName(), "inapp", null);
int response = ownedItems.getInt("RESPONSE_CODE");
if (response == 0) {
   ArrayList<String> ownedSkus =
      ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
   ArrayList<String>  purchaseDataList =
      ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
   ArrayList<String>  signatureList =
      ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE_LIST");
   String continuationToken =
      ownedItems.getString("INAPP_CONTINUATION_TOKEN");

   for (int i = 0; i < purchaseDataList.size(); ++i) {
      String purchaseData = purchaseDataList.get(i);
      String signature = signatureList.get(i);
      String sku = ownedSkus.get(i);

      // do something with this purchase information
      // e.g. display the updated list of products owned by user
   }

   // if continuationToken != null, call getPurchases again
   // and pass in the token to retrieve more items
}

인앱 상품 세부정보 확인
Bundle skuDetails = mService.getSkuDetails(3,
   getPackageName(), "inapp", querySkus);
int response = skuDetails.getInt("RESPONSE_CODE");
if (response == 0) {
   ArrayList<String> responseList
      = skuDetails.getStringArrayList("DETAILS_LIST");

   for (String thisResponse : responseList) {
      JSONObject object = new JSONObject(thisResponse);
      String sku = object.getString("productId");
      String price = object.getString("price");
      if (sku.equals("premiumUpgrade")) mPremiumUpgradePrice = price;
      else if (sku.equals("gas")) mGasPrice = price;
   }
}

인앱 상품 구매
(요청)
Bundle buyIntentBundle = mService.getBuyIntent(3, getPackageName(),
   sku, "inapp", "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ");
PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
startIntentSenderForResult(pendingIntent.getIntentSender(),
   1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0),
   Integer.valueOf(0));
(응답)
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   if (requestCode == 1001) {
      int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
      String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
      String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");

      if (resultCode == RESULT_OK) {
         try {
            JSONObject jo = new JSONObject(purchaseData);
            String sku = jo.getString("productId");
            alert("You have bought the " + sku + ". Excellent choice,
               adventurer!");
          }
          catch (JSONException e) {
             alert("Failed to parse purchase data.");
             e.printStackTrace();
          }
      }
   }
}
(구매 정보)
'{
   "orderId":"GPA.1234-5678-9012-34567",
   "packageName":"com.example.app",
   "productId":"exampleSku",
   "purchaseTime":1345678900000,
   "purchaseState":0,
   "developerPayload":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
   "purchaseToken":"opaque-token-up-to-1000-characters"
 }'

* 구독에 대한 구매 흐름은 유사하지만 상품 유형이 subs로 설정해야 함.
(요청)
Bundle bundle = mService.getBuyIntent(3, "com.example.myapp",
   MY_SKU, "subs", developerPayload);
PendingIntent pendingIntent = bundle.getParcelable(RESPONSE_BUY_INTENT);
if (bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) {
   // Start purchase flow (this brings up the Google Play UI).
   // Result will be delivered through onActivityResult().
   startIntentSenderForResult(pendingIntent, RC_BUY, new Intent(),
       Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
}
(구매 확인)
Bundle activeSubs = mService.getPurchases(3, "com.example.myapp",
                   "subs", continueToken);

* Transaction 무결성을 보장하기 위해 Google Play는 구매 응답 데이터를 포함한 JSON 문자열에 서명함. 


[인앱 상품 소비]

구매한 상품은 소유된 상태로 간주되고 소유된 상태는 Google Play에서 다시 구매할 수 없음. 하지만 소유한 상품을 소비 하면 구매 데이터가 삭제 되고 소유되지 않는 상태로 변경됨.


int response = mService.consumePurchase(3, getPackageName(), token);


[구독]

[홍보]

[보안 및 디자인]

[테스트]

[관리]

2017년 1월 16일 월요일

Android에서 REST client 구현 관련.. TLS 1.2 지원

예전에 구현했던 것을 다시 업데이트 하다 보니
많은 부분이 변경이 되어 관련 정리를 해봄.

일단 REST architecture 개념은 다음 논문을 참고하시라.
http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm


먼저 HTTPS 연결이 안드로이드 내부에서 자동으로 되는 것 같음.
예전에는 별도 class를 만들어서 연결 했었던 것 같은데

https://developer.android.com/training/articles/security-ssl.html#HttpsExample

잘 알려진 CA가 발급한 인증서가 여러분의 웹 서버에 있다고 가정하면, 다음과 같은 간단한 코드로 보안 요청을 수행할 수 있습니다.
URL url = new URL("https://wikipedia.org");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
그렇습니다. 정말로 이렇게 간단합니다. 맞춤형 HTTP 요청을 수행하려면, HttpURLConnection으로 캐스트하면 됩니다. HttpURLConnection의 Android 문서에는 더 자세한 예시가 있으며, 이 예시에서는 요청과 응답 헤더 처리, 콘텐츠 게시, 쿠키 관리, 프록시 사용, 응답 캐싱 등에 대해 설명합니다. 그러나 인증서와 호스트 이름을 확인하는 자세한 절차는 Android 프레임워크가 이러한 API를 통해 자동으로 처리합니다. 가능하면 여기에서 모든 것을 해결할 수 있습니다. 하지만, 아래와 같은 다른 고려사항도 있습니다.

기본적인 HTTPURLConnection 사용법은 다음과 같음.

https://developer.android.com/reference/java/net/HttpURLConnection.html

URL url = new URL("http://www.android.com/");
   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
   try {
     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
     readStream(in);
   } finally {
     urlConnection.disconnect();
   }

정말 간단하고
REST client를 만들기 위해서 필요한 method 조정이나 upload 관련 부분도 API를 지원해서 간단하게 처리할 수 있을 것 같음.

HTTP Methods

HttpURLConnection uses the GET method by default. It will use POST if setDoOutput(true) has been called. Other HTTP methods (OPTIONSHEADPUTDELETE and TRACE) can be used with setRequestMethod(String).

Posting Content

To upload data to a web server, configure the connection for output using setDoOutput(true).
For best performance, you should call either setFixedLengthStreamingMode(int) when the body length is known in advance, or setChunkedStreamingMode(int) when it is not. Otherwise HttpURLConnection will be forced to buffer the complete request body in memory before it is transmitted, wasting (and possibly exhausting) heap and increasing latency.
For example, to perform an upload:
   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
   try {
     urlConnection.setDoOutput(true);
     urlConnection.setChunkedStreamingMode(0);

     OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
     writeStream(out);

     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
     readStream(in);
   } finally {
     urlConnection.disconnect();
   }


근데...

TLS 1.2 지원에 좀 문제가 있는지 지원을 안하는지..
개발 중인 서비스 서버에 연결이 제대로 안된다.
일단 예전에 사용하던 방식을 다시 사용하긴 했는데 찾아보니 다른 방법들도 꽤 있는 것 같음.

일단은 그냥 사용하게 된 예전 코드
 : https://github.com/hallower/wimple/blob/master/app/src/main/java/kr/blogspot/charlie0301/wimple/impl/RestAPIInvoker.java#L49