2026/02/27

오늘의 이야기

최종 추천 번호:
추천 [16,27,30,31,38,39]
추천 [02,04,18,29,33,40]
추천 [03,11,17,25,32,45]
추천 [07,15,24,28,34,42]
추천 [09,19,26,35,41,43]

### 다음 라운드 번호 추천 분석

#### 1. 번호 출현 빈도 분석
전체 회차(20회) 번호별 출현 빈도:
01: 5회, 02: 2회, 03: 4회, 04: 2회, 05: 3회, 06: 3회, 07: 4회, 08: 3회, 09: 3회, 10: 2회, 11: 1회, 12: 2회, 13: 1회, 15: 3회, 16: 5회, 17: 4회, 18: 1회, 19: 1회, 20: 2회, 21: 1회, 22: 1회, 23: 3회, 24: 4회, 25: 2회, 26: 3회, 27: 6회, 28: 3회, 29: 2회, 30: 4회, 31: 4회, 32: 2회, 33: 4회, 34: 2회, 35: 4회, 36: 4회, 37: 4회, 38: 5회, 39: 4회, 40: 3회, 41: 3회, 42: 2회, 43: 1회, 44: 2회, 45: 2회
가장 많이 출현한 번호 6개: [16,27,30,31,38,39]

#### 2. 홀/짝 비율 분석
회차별 홀/짝 조합 빈도:
짝 3개/홀 3개: 9회, 짝 2개/홀 4개: 5회, 짝 4개/홀 2개: 5회, 짝 1개/홀 5개: 1회
가장 빈번한 홀/짝 조합은 짝 3개, 홀 3개입니다. 마지막 라운드는 짝 3개, 홀 3개였습니다.

#### 3. 총합 및 평균 분석
역대 총합 범위: 104 - 187 (평균: 147.25)
역대 평균값 범위: 17.33 - 31.17 (평균: 24.54)
마지막 라운드(1212) 총합: 154, 평균: 25.67

#### 4. 간격 패턴 분석
번호 간격별 출현 빈도:
1: 16회, 2: 13회, 3: 16회, 4: 5회, 5: 6회, 6: 7회, 7: 6회, 8: 7회, 9: 2회, 10: 4회, 11: 2회, 12: 2회, 13: 1회, 14: 1회, 15: 3회, 16: 1회, 17: 3회, 18: 1회, 21: 1회, 22: 1회, 24: 1회, 25: 1회, 26: 1회, 30: 1회
마지막 라운드(1212) 간격: [3, 17, 6, 10, 3]
작은 간격(1-5)과 중간 간격(6-15)의 혼합이 자주 관찰됩니다.

#### 5. 패턴 일치도 및 주기 분석
과거 데이터를 통해 총합, 평균, 홀/짝 비율, 간격 패턴 등 4가지 속성을 기준으로 패턴 일치도를 분석했습니다. 일부 속성에서 동일 패턴 반복 주기가 관찰되었으나, 20회차 데이터만으로는 강력한 주기성을 단정하기 어렵습니다. 다만, 특정 패턴(예: 3짝 3홀 조합, 중간 범위의 총합)이 자주 나타나는 경향이 있습니다.

### 추천 조합 생성 근거

**추천1 [16,27,30,31,38,39]**: 전체 20회차 데이터에서 가장 높은 출현 빈도를 보인 6개의 번호를 조합했습니다.
**추천2 [02,04,18,29,33,40]**: 과거 당첨 번호의 통계적 균형(예: 짝수 3개, 홀수 3개, 중간 범위의 총합)을 고려하여 선정했습니다.
**추천3 [03,11,17,25,32,45]**: 최근 라운드의 번호 간격 패턴(작은 간격, 중간 간격, 큰 간격의 조화)을 반영하여 번호의 분포를 예측했습니다.
**추천4 [07,15,24,28,34,42]**: 출현 빈도가 높은 '핫'한 번호와 상대적으로 출현이 적은 '콜드'한 번호를 균형 있게 조합하여 선정했습니다.
**추천5 [09,19,26,35,41,43]**: 과거 특정 당첨 라운드(예: 1201회차)의 주요 특징(홀/짝 비율, 총합, 평균 등)을 모방하여 유사한 통계적 특성을 가지는 번호 조합을 생성했습니다.

### 마지막 라운드(1212회차)와 추천 조합 비교 분석

**1212회차 당첨 번호**: [05,08,25,31,41,44]
- 총합: 154, 평균: 25.67
- 홀/짝: 짝수 3개, 홀수 3개
- 간격: [3, 17, 6, 10, 3]

**추천1 조합 ([16,27,30,31,38,39]) 분석**:
- 총합: 181, 평균: 30.17 (1212회차 총합 154, 평균 25.67)
- 홀/짝: 짝수 3개, 홀수 3개 (1212회차 짝수 3개, 홀수 3개)
- 간격: [11, 3, 1, 7, 1] (1212회차 간격 [3, 17, 6, 10, 3])
- **1212회차 대비 특징**: 총합이 다소 차이 남, 홀/짝 비율이 동일함. 이 조합은 1212회차와 번호 구성이 다르며, 최근 10회차 당첨 번호와도 중복되지 않습니다.

**추천2 조합 ([02,04,18,29,33,40]) 분석**:
- 총합: 126, 평균: 21.00 (1212회차 총합 154, 평균 25.67)
- 홀/짝: 짝수 3개, 홀수 3개 (1212회차 짝수 3개, 홀수 3개)
- 간격: [2, 14, 11, 4, 7] (1212회차 간격 [3, 17, 6, 10, 3])
- **1212회차 대비 특징**: 총합이 다소 차이 남, 홀/짝 비율이 동일함. 이 조합은 1212회차와 번호 구성이 다르며, 최근 10회차 당첨 번호와도 중복되지 않습니다.

**추천3 조합 ([03,11,17,25,32,45]) 분석**:
- 총합: 133, 평균: 22.17 (1212회차 총합 154, 평균 25.67)
- 홀/짝: 짝수 2개, 홀수 4개 (1212회차 짝수 3개, 홀수 3개)
- 간격: [8, 6, 8, 7, 13] (1212회차 간격 [3, 17, 6, 10, 3])
- **1212회차 대비 특징**: 총합이 다소 차이 남, 홀/짝 비율이 다름, 일부 간격 패턴이 1212회차와 겹침. 이 조합은 1212회차와 번호 구성이 다르며, 최근 10회차 당첨 번호와도 중복되지 않습니다.

**추천4 조합 ([07,15,24,28,34,42]) 분석**:
- 총합: 150, 평균: 25.00 (1212회차 총합 154, 평균 25.67)
- 홀/짝: 짝수 3개, 홀수 3개 (1212회차 짝수 3개, 홀수 3개)
- 간격: [8, 9, 4, 6, 8] (1212회차 간격 [3, 17, 6, 10, 3])
- **1212회차 대비 특징**: 총합이 유사함, 홀/짝 비율이 동일함, 일부 간격 패턴이 1212회차와 겹침. 이 조합은 1212회차와 번호 구성이 다르며, 최근 10회차 당첨 번호와도 중복되지 않습니다.

**추천5 조합 ([09,19,26,35,41,43]) 분석**:
- 총합: 173, 평균: 28.83 (1212회차 총합 154, 평균 25.67)
- 홀/짝: 짝수 1개, 홀수 5개 (1212회차 짝수 3개, 홀수 3개)
- 간격: [10, 7, 9, 6, 2] (1212회차 간격 [3, 17, 6, 10, 3])
- **1212회차 대비 특징**: 총합이 다소 차이 남, 홀/짝 비율이 다름. 이 조합은 1212회차와 번호 구성이 다르며, 최근 10회차 당첨 번호와도 중복되지 않습니다.



사용하는 예시 영상 보기
이 앱이 궁금 하다면, 아래 링크에서 설치할 수 있습니다.
로또 645






오늘의 이야기

 


https://developer.android.com/guide/components/broadcasts?hl=ko 



 


브로드캐스트 개요  |  Android 개발자  |  Android Developers


브로드캐스트 개요 Android 앱은 Android 시스템 및 기타 Android 앱에서 게시-구독 디자인 패턴과 유사한 브로드캐스트 메시지를 받거나 보낼 수 있습니다. 관심 있는 이벤트가 발생할 때 이러한 브로


developer.android.com




앱을 만들다 보니 구글이 싫어하는 암시적 intent 설정에 대한 이슈가 있었다. 그래서 찾아보다 알게된 것...


특히 앱에서 broadcasting 을 하게 되면, 구글은 모든앱이 받는 이슈에 대해서 싫어 한다. 


 


그래서 이제 부터는 모든 것을 명시적으로 선언해 주어야 만 하는 난관(?)에 봉착하게 된다.  앞으로는 개발하는 게 더 힘들어 질 것 같다. 흑~


 


    Intent intent = new Intent();
    intent.setAction("com.example.broadcast.MY_NOTIFICATION");
    intent.putExtra("data","Notice me senpai!");
intent.setPackage(getApplicationPackageName());
    sendBroadcast(intent);

앞으로는 이렇게 그냥 나의 앱 packageName 을 명시적으로 선언해 주는 습관을 드려야 할 것 같다.


 


playstore 에 앱을 게시하고자 올리면 테스트 단계를 거치는 데, 7~8 시간 대기가 필요하게 되므로... 시간 절약을 위해서.


 





오늘의 이야기

오늘은 내 앱의 화면에 올라온 내용중에 webView 에 들어 있는 것을 이미지 파일로 만들고 pdf 파일에 담아서 공유하는 기능을 구현해 보고자 한다. 


 


먼저할 것은 인터넷 접속을 위한 권한 부여


 


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

굳이 사용자에게 허가를 받지 않아도 된다.  그 다음에 layout 을 구성해 보았다. 


 


변환할 webView 에시



보는 것 처럼 화면에는 버튼 한개와 webView 만 한개 만들어 두었다.


 


layout의 코드는 다음과 같이.


 


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<Button
android:id="@+id/pdfShare"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="16dp"
android:text="Button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<WebView
android:id="@+id/webView"
android:layout_width="0dp"
android:layout_height="645dp"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pdfShare" />

</androidx.constraintlayout.widget.ConstraintLayout>

 


그 다음은 이미지를 임시로 저장하고 그걸 pdf 로 전환하는 방법을 구현할 예정이기 때문에 임시저장소를 선택할 수 있도록 file_provider 의 값을 등록해 준다.  res/xml 폴더에 file_provider.xml 로 아래 내용을 저장해 주었다. 


cache 을 사용하는 것은  파일 저장을 하기 위해서 사용자에게 권한을 허가 받지 않아도 되기 떄문이다. 


<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cache" path="."/>
</paths>

 


다음은 activity을 구현해 보아야겠다.


먼저 webView 에 나의 블로그를 load할 수 있도록 하고, load가 다 되었다면 그 때 이미지 파일에 임시 저장을 한 다음, 


버튼을 클릭하면 그 이미지 파일을 열어서 공유하는 intent 을 호출 하도록 구현할 예정이다. 


 



import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.pdf.PdfDocument;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

import com.billcoreatech.pdftest.databinding.ActivityMainBinding;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";
Bitmap bitmap, scalebmp ;
int pageWidth = 1200 ;
ActivityMainBinding bind ;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
bind = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(bind.getRoot());

bind.webView.loadUrl("https://billcorea.tistory.com");
bind.webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
new Background().execute();
}
});

bind.pdfShare.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (createPDF ()) {
File pdfFile = new File(getCacheDir(), "/pizza.pdf");
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), getApplicationContext().getPackageName()+".fileProvider", pdfFile);

Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("application/pdf");
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(shareIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
grantUriPermission(packageName, contentUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
startActivity(Intent.createChooser(shareIntent, "PDF 공유"));
};
}
});

}

private boolean createPDF() {

File file = new File(getCacheDir(), "/aaaa.png");
if (!file.isFile()) {
Toast.makeText(getApplicationContext(), "파일 생성이 되지 않았습니다.", Toast.LENGTH_SHORT).show();
return false ;
}

bitmap = BitmapFactory.decodeFile(getCacheDir().toString() + "/aaaa.png");

PdfDocument pdfDocument = new PdfDocument();
Paint paint = new Paint();

PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(bitmap.getWidth(), bitmap.getHeight(), 1).create();
PdfDocument.Page page = pdfDocument.startPage(pageInfo);
Canvas canvas = page.getCanvas();
canvas.drawBitmap(bitmap, 0, 0, paint);

pdfDocument.finishPage(page);

File file1 = new File(getCacheDir(), "/pizza.pdf");
try {
pdfDocument.writeTo(new FileOutputStream(file1));
} catch (IOException e) {
return false ;
}
pdfDocument.close();

return true ;
}

class Background extends AsyncTask<Void, Void, Bitmap>
{
@Override
protected Bitmap doInBackground(Void... params)
{
try
{
Thread.sleep(2000);
Bitmap bitmap = Bitmap.createBitmap(bind.webView.getWidth(), bind.webView.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
bind.webView.draw(canvas);
return bitmap;
}
catch (InterruptedException e){}
catch (Exception e){}
return null;
}
@SuppressLint("WrongThread")
@Override
protected void onPostExecute(Bitmap bm)
{
try {
OutputStream fOut = null;
File file = new File(getCacheDir(), "/aaaa.png");
fOut = new FileOutputStream(file);

bm.compress(Bitmap.CompressFormat.PNG, 50, fOut);
fOut.flush();
fOut.close();
bm.recycle();

Log.e(TAG, "fileName=" + getCacheDir() + "/aaaa.png");

Toast.makeText(getApplicationContext(), "공유할 파일이 생성되었습니다.", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();

Log.e(TAG, "" + e.toString());
}
}
}
}

 


webView 에 load는 시간이 소요 되기 떄문에 Background 로 load 된 이미지를 png 로 변환하도록 구현이 되었고, 


버튼을 클릭하면 그 때 png 파일이 있는 지 체크 해서 없으면 알림을 보여주고, 있으면 해당 이미지 파일을 pdf 로 전환해서 하고, pdf 가 생성되었다면, intent 을 호출해서 공유하도록 구현하는 방식이다. 


 


다만, API 32 기준으로 AVD에서 테스트를 해 보면서 오류 로그가 보이기는 했으나, 실행은 되었다. provider 에게 uri read 권한을 주어야 할 것 같은데, 아직 구글링을 했을 때 정확한 해소 방안은 찾을 수 없었다.


 


어떻게 하지 ?


 


 



실행 예시


 


알게될 날이 올까 ?


 


오류 해소를 위해서 다음 링크 까지는 해 보았으나... 아직 ... ㅠㅠ;;


 


https://stackoverflow.com/questions/57689792/permission-denial-while-sharing-file-with-fileprovider/59439316#59439316



 


Permission Denial while sharing file with FileProvider


I am trying to share file with FileProvider. I checked that file is shared properly with apps like Gmail, Google Drive etc. Even though following exception is thrown: 2019-08-28 11:43:03.169 12573-...


stackoverflow.com




 





오늘의 이야기



#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase

🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 "혹시나~" 하면서 봤거든 ㅋㅋ

근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨

그것도 구글 Gemini로다가!

그래서 앱 하나 만들었지
👉 "로또 예상번호 by Gemini" 🎱

AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨

재미로 해도 좋고…
혹시 모르는 거잖아? 😏


https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127




오늘의 이야기

이전 포스팅


이전에 작성했던 포스팅을 참고하여 인앱 결제를 구현했던 기억을 되살펴 보겠습니다.

https://billcorea.tistory.com/27







안드로이드 앱 만들기 구글 인앱결제 쉽게 따라 하기...


인앱 결제를 하기 위해서 오늘도 구글링을 하시는 분들께... 기본적은 헤맴을 줄여보기 위해서 정리를 해 둡니다. 인앱 결제를 하려면 일단, 할 일은 앱을 하나 만들어서 구글 플레이에 등록을


billcorea.tistory.com





이전 포스팅에서는 1회성 결제에 대한 구현을 살펴볼 수 있습니다. 이번에는 정기 결제를 구현해 보도록 하겠습니다. 이번 구현을 위해서 gradle 설정을 해 봅니다.


dependencies {

    ...
    
    implementation 'com.android.billingclient:billing:5.0.0'
    implementation 'com.google.code.gson:gson:2.9.0'
    
    ...

}


BillingClient


코드 구현은 다음과 같이 구현을 하였습니다. 코드 하나 구현해 두면 다음 프로젝트에서는 그대로 옮겨다 사용할 수 있습니다.


import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchaseHistoryRecord;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.PurchasesResponseListener;
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 com.billcoreatech.daycnt415.R;
import com.billcoreatech.daycnt415.util.KakaoToast;
import com.billcoreatech.daycnt415.util.StringUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class BillingManager implements PurchasesUpdatedListener, ConsumeResponseListener {
    String TAG = "BillingManager" ;
    BillingClient mBillingClient ;
    Activity mActivity ;
    public List<SkuDetails> mSkuDetails ;

    public enum connectStatusTypes { waiting, connected, fail, disconnected }
    public connectStatusTypes connectStatus = connectStatusTypes.waiting ;
    private ConsumeResponseListener mConsumResListnere ;

    String punchName = "210414_monthly_bill_999";
    String payType = BillingClient.SkuType.SUBS ;

    SharedPreferences option ;
    SharedPreferences.Editor editor ;

    public BillingManager (Activity _activity) {
        mActivity = _activity ;
        option = mActivity.getSharedPreferences("option", mActivity.MODE_PRIVATE);
        editor = option.edit();
        mBillingClient = BillingClient.newBuilder(mActivity)
                .setListener(this)
                .enablePendingPurchases()
                .build() ;
        mBillingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
                Log.e(TAG, "respCode=" + billingResult.getResponseCode() ) ;
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    connectStatus = connectStatusTypes.connected ;
                    Log.e(TAG, "connected...") ;
                    purchaseAsync();
                    getSkuDetailList();

                } else {
                    connectStatus = connectStatusTypes.fail ;
                    Log.i(TAG, "connected... fail ") ;
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                connectStatus = connectStatusTypes.disconnected ;
                Log.i(TAG, "disconnected ") ;
            }
        });

    }

    /**
     * 정기 결재 소모 여부를 수신 : 21.04.20 1회성 구매의 경우는 결재하면 끝임.
     * @param billingResult
     * @param purchaseToken
     */
    @Override
    public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String purchaseToken) {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
            Log.i(TAG, "사용끝 + " + purchaseToken) ;
            return ;
        } else {
            Log.i(TAG, "소모에 실패 " + billingResult.getResponseCode() + " 대상 상품 " + purchaseToken) ;
            return ;
        }
    }

    public int purchase(SkuDetails skuDetails) {
        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build();
        return mBillingClient.launchBillingFlow(mActivity, flowParams).getResponseCode();
    }

    public void purchaseAsync() {
        Log.e(TAG, "--------------------------------------------------------------");

        mBillingClient.queryPurchasesAsync(payType, new PurchasesResponseListener() {
            @Override
            public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> list) {
                Log.e(TAG, "onQueryPurchasesResponse=" + billingResult.getResponseCode()) ;
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                if (list.size() < 1) {
                    editor = option.edit();
                    editor.putBoolean("isBill", false);
                    editor.commit();
                } else {
                    for (Purchase purchase : list) {
                        Log.e(TAG, "getPurchaseToken=" + purchase.getPurchaseToken());
                        for (String str : purchase.getSkus()) {
                            Log.e(TAG, "getSkus=" + str);
                        }
                        Date now = new Date();
                        now.setTime(purchase.getPurchaseTime());
                        Log.e(TAG, "getPurchaseTime=" + sdf.format(now));
                        Log.e(TAG, "getQuantity=" + purchase.getQuantity());
                        Log.e(TAG, "getSignature=" + purchase.getSignature());
                        Log.e(TAG, "isAutoRenewing=" + purchase.isAutoRenewing());

                        editor = option.edit();
                        editor.putBoolean("isBill", purchase.isAutoRenewing());
                        editor.commit();
                    }
                }
            }
        });

        mBillingClient.queryPurchaseHistoryAsync(payType, new PurchaseHistoryResponseListener() {
            @Override
            public void onPurchaseHistoryResponse(@NonNull BillingResult billingResult, @Nullable List<PurchaseHistoryRecord> list) {
                if (billingResult.getResponseCode() == 0) {
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    for(PurchaseHistoryRecord purchase : list) {
                        Log.e(TAG, "getPurchaseToken=" + purchase.getPurchaseToken());
                        for (String str : purchase.getSkus()) {
                            Log.e(TAG, "getSkus=" + str);
                        }
                        Date now = new Date();
                        now.setTime(purchase.getPurchaseTime());
                        Log.e(TAG, "getPurchaseTime=" + sdf.format(now));
                        Log.e(TAG, "getQuantity=" + purchase.getQuantity());
                        Log.e(TAG, "getSignature=" + purchase.getSignature());

                        if (payType.equals(BillingClient.SkuType.INAPP)) {
                            ConsumeParams params = ConsumeParams.newBuilder()
                                    .setPurchaseToken(purchase.getPurchaseToken())
                                    .build();
                            mBillingClient.consumeAsync(params, BillingManager.this);
                        }

                    }
                }
            }
        });
    }

    public void getSkuDetailList() {
        List<String> skuIdList = new ArrayList<>() ;
        skuIdList.add(punchName);

        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(skuIdList).setType(payType);
        mBillingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
            @Override
            public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> skuDetailsList) {
                if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
                    Log.i(TAG, "detail respCode=" + billingResult.getResponseCode()) ;
                    return ;
                }
                if (skuDetailsList == null) {
                    KakaoToast.makeToast(mActivity, mActivity.getString(R.string.msgNotInfo), Toast.LENGTH_LONG).show();
                    return ;
                }
                Log.i(TAG, "listCount=" + skuDetailsList.size());
                for(SkuDetails skuDetails : skuDetailsList) {
                    Log.i(TAG, "\n" + skuDetails.getSku()
                            + "\n" + skuDetails.getTitle()
                            + "\n" + skuDetails.getPrice()
                            + "\n" + skuDetails.getDescription()
                            + "\n" + skuDetails.getFreeTrialPeriod()
                            + "\n" + skuDetails.getIconUrl()
                            + "\n" + skuDetails.getIntroductoryPrice()
                            + "\n" + skuDetails.getIntroductoryPriceAmountMicros()
                            + "\n" + skuDetails.getOriginalPrice()
                            + "\n" + skuDetails.getPriceCurrencyCode()) ;
                }
                mSkuDetails = skuDetailsList ;

            }
        });
    }

    /**
     * @param billingResult
     * @param purchases
     */
    @Override
    public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {

        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
            Log.i(TAG, "구매 성공>>>" + billingResult.getDebugMessage());
            JSONObject object = null ;
            String pID = "" ;
            String pDate = "" ;

            for(Purchase purchase : purchases) {
                // 정기구독의 경우는 구매처리후 구매 확인을 해 주어야 취소가 되지 않음.
                handlePurchase(purchase);
                Log.i(TAG, "성공값=" + purchase.getPurchaseToken()) ;
                try {
                    Log.e(TAG, "getOriginalJson=" + purchase.getOriginalJson());
                    object = new JSONObject(purchase.getOriginalJson());
                    String sku = "";
                    for (String str : purchase.getSkus()) {
                        sku = str ;
                        Log.e(TAG, "SKU=" + sku);
                    }
                    pID = object.getString("purchaseToken");
                    pDate = StringUtil.getDate(object.getLong("purchaseTime"));
                    if (sku.equals(punchName)) {                      
                        editor.putLong("billTimeStamp", object.getLong("purchaseTime"));
                        editor.putBoolean("isBill", object.getBoolean("autoRenewing"));
                        editor.putString("token", purchase.getPurchaseToken());
                    }
                    editor.commit();

                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
            Log.i(TAG, "결제 취소");
            editor = option.edit();
            editor.putBoolean("isBill", false);
            editor.commit();
        } else {
            Log.i(TAG, "오류 코드=" + billingResult.getResponseCode()) ;
            editor = option.edit();
            editor.putBoolean("isBill", false);
            editor.commit();
        }
    }

    void handlePurchase(Purchase purchase) {
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams =
                        AcknowledgePurchaseParams.newBuilder()
                                .setPurchaseToken(purchase.getPurchaseToken())
                                .build();
                mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
                    @Override
                    public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
                        Log.e(TAG, "getResponseCode=" + billingResult.getResponseCode());
                    }
                });
            }
        }
        //PENDING
        else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
            //구매 유예
            Log.e(TAG, "//구매 유예");
        }
        else {
            //구매확정 취소됨(기타 다양한 사유...)
            Log.e(TAG, "//구매확정 취소됨(기타 다양한 사유...)");
        }    
    }

}

정기결제 예시


정기결제를 실행하는 화면은 다음과 왼쪽 그림과 같이 처리가 됩니다.

이렇게 정기 결제된 경우에는 다른 처리를 할 필요는 없습니다.

다만, 정기결제가 유지되고 있는 지를 확인하는 처리가 필요했습니다.

그래서 아래 코드와 같이 queryPurchaseAsync을 호출해서
구매된 내역을 확인하여 그 값 중에

isAutoRenewing()의 값이 true 가 오는 지를 보고 값이 true 인 경우는 정기결제가 유지되고 있음을 확인할 수 있었습니다.

list 값이 오지 않거나, false 가 오면 정기 구매가 되지 않고 있다고 보고 필요한 처리를 하면 됩니다.














mBillingClient.queryPurchasesAsync(payType, new PurchasesResponseListener() {
            @Override
            public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> list) {
                Log.e(TAG, "onQueryPurchasesResponse=" + billingResult.getResponseCode()) ;
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                if (list.size() < 1) {
                    editor = option.edit();
                    editor.putBoolean("isBill", false);
                    editor.commit();
                } else {
                    for (Purchase purchase : list) {
                        Log.e(TAG, "getPurchaseToken=" + purchase.getPurchaseToken());
                        for (String str : purchase.getSkus()) {
                            Log.e(TAG, "getSkus=" + str);
                        }
                        Date now = new Date();
                        now.setTime(purchase.getPurchaseTime());
                        Log.e(TAG, "getPurchaseTime=" + sdf.format(now));
                        Log.e(TAG, "getQuantity=" + purchase.getQuantity());
                        Log.e(TAG, "getSignature=" + purchase.getSignature());
                        Log.e(TAG, "isAutoRenewing=" + purchase.isAutoRenewing());

                        editor = option.edit();
                        editor.putBoolean("isBill", purchase.isAutoRenewing());
                        editor.commit();
                    }
                }
            }
        });


테스트 구매의 경우는 5분마다 한 번씩 갱신되므로 5분 뒤에 다시 확인하는 처리에 대한 검증을 해 볼 수 있습니다.
기타 추가적인 작업을 해야 할 것들이 남아 있는 것으로 생각이 됩니다. 아래 링크를 참고해서 추가 구현을 해 보도록 하겠습니다.

https://developer.android.com/google/play/billing/subscriptions?hl=ko







정기 결제 판매  |  Google Play 결제 시스템  |  Android Developers


알림: 2021년 11월 1일부터는 기존 앱의 모든 업데이트에도 결제 라이브러리 버전 3 이상이 요구됩니다. 자세히 알아보기 정기 결제 판매 이 주제에서는 갱신 및 만료와 같은 정기 결제 수명 주기


developer.android.com





이것으로 인앱 결제 정기결제에 대한 이해를 해 보았습니다.

정기구독의 경우 구독취소가 되지 않도록 하기 위해서는 구매한 것을 확인해 주는 처리를 꼭 거쳐야 합니다. 그렇지 않을 경우에는 구독했던 것이 취소되어 소모가 되지 않기 때문입니다.


if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
Log.e(TAG, "getResponseCode=" + billingResult.getResponseCode());
}
});
}
}
//PENDING
else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
//구매 유예
Log.e(TAG, "//구매 유예");
}
else {
//구매확정 취소됨(기타 다양한 사유...)
Log.e(TAG, "//구매확정 취소됨(기타 다양한 사유...)");
}


코드공유


여기까지 구현된 소스는 github에서 참고해 보시기 바랍니다.

https://github.com/nari4169/daycnt415







GitHub - nari4169/daycnt415


Contribute to nari4169/daycnt415 development by creating an account on GitHub.


github.com





마치며


테스트는 앱을 내부테스트로 게시한 이후에 진행하세요. 꼭이요~~~

p.s 이글은 예전에 포스팅 했던 내용을 다시 수정해서 올렸습니다.





오늘의 이야기


#스하리1000명프로젝트

스치니들!
내가 만든 이 앱은, 내 폰에 오는 알림 중에서 중요한 키워드가 있는 경우
등록해둔 친구에게 자동으로 전달해주는 앱이야 📲

예를 들어, 카드 결제 알림을 와이프나 자녀에게 보내주거나
이번 달 지출을 달력처럼 확인할 수도 있어!

앱을 함께 쓰려면 친구도 설치 & 로그인해줘야 해.
그래야 친구 목록에서 서로 선택할 수 있으니까~
서로 써보고 불편한 점 있으면 알려줘 🙏

👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao





오늘의 이야기

 


google map platform 에서 온 메일



 


오늘은 google map platform 에서 알려온 메일에 대해서 잠시 보고 갈까 한다.  앱을 개발 하다 보니 google map 을 활용하게 되는 데, 이런 메일이 오다니... 


 


메일의 본문은 아래와 같이 읽어 보게 되었다. 


 


--- 메일 번역문 ---


일부 프로젝트는 Android용 Maps SDK를 사용합니다. 개선된 렌더러 및 기타 기능을 테스트하려면 새 업그레이드를 시도하십시오.

안녕하세요, Google Maps Platform 고객입니다.


Google Play 서비스 를 통해 제공되는 Android용 Maps SDK (버전 18.0.x)  주요 업데이트 에는 점진적 출시를 통해 Android 기기*의 기본 렌더러가 될 새로운 지도 렌더러가 포함되어 있음을 알려 드립니다. 빠르면 2022년 6월 .


새 렌더러가 기본값이 되면 앱은 Android용 Maps SDK 버전 18.0.x 에 대해 빌드되었는지 여부에 관계없이 새 렌더러를 사용합니다.


새로운 맵 렌더러(SDK 최신 버전: 18.0.2)로 업데이트하여 새 렌더러가 Android 기기*의 기본 렌더러가 되기 전에 앱과 함께 새 SDK 및 렌더러를 테스트하고 배포하세요.


새로운 SDK를 사용하여 앱에 타일을 제공하고 클라이언트 장치에서 지도를 렌더링하는 방법을 개선했습니다. 따라서 Android 장치는 다음과 같은 이점이 있습니다.



  • 네트워크 부하, 처리 요구 사항 및 메모리 소비 감소.

  • 더 나은 애니메이션과 더 부드러운 이동 및 확대/축소를 위한 향상된 제스처 처리.

  • 보다 유동적인 전환 및 명확하게 배치된 지도 레이블.

  • Android의 향후 지도 혁신을 위해 더욱 안정적이고 개선된 사용자 경험.


새 SDK에는 모바일에서 클라우드 기반 지도 스타일링의 일반 가용성도 포함됩니다. Android용 Maps SDK 의 지도에 대한 클라우드 기반 지도 스타일 사용 은 Dynamic Maps SKU 로 청구됩니다 . 가격 및 포함된 기능에 대한 자세한 내용은 Cloud Customization 설명서를 참조하십시오.


* 데이터 저장 공간이 2GB 이상인 Android 5.0(Lollipop) 이상의 모든 장치는 새 렌더러를 사용합니다. 다른 모든 장치는 추후 공지가 있을 때까지 레거시 렌더러를 계속 사용합니다.


내가 무엇을해야 하나?


앱의 문제를 예상하고 방지하려면 다음을 수행하는 것이 좋습니다 .



  1. 종속 항목에 버전 18.0.x를 지정합니다( 예시 ).

  2. 코드에서 새 렌더러를 지정합니다( 지침 ).

  3. 2022년 6월 에 새로운 SDK 점진적 출시가 시작되기 전에 새 렌더러를 기반으로 앱의 새 버전을 테스트, 빌드 및 출시하세요. 새 렌더러를 사용하는 방법에 대한 지침은 새 맵 렌더러 설명서를 참조하세요 .


새 SDK 또는 새 렌더러를 사용하는 앱에서 문제가 발생하면 Android용 Maps SDK 지원 옵션을 확인 하고 문제가 해결될 때까지 사용자에 대한 앱 배포를 일시 중지하는 것이 좋습니다.


아래 나열된 프로젝트 에서 지난 4주 동안 Android용 Maps SDK를 사용하고 있는 것으로 확인되었습니다.


 


--- 번역문 끝 ---


 


말인즉슨 내가 만든앱에서 google map 을 사용하고 있고, 2022.6월이 지나면 새로운 renderer 을 사용하게 될 것이니 그전에 미리 sdk 버전을 18.0.x 버전으로 upgrade 하라는 말이다.  구글에서 새로운 renderer 를 사용하게 되면 4가지 정도 이상의 이점이 있다니, 해야지 얼른 내가 만들어 playstore 에 게시한 앱들을 살펴 봐야 겠다. 


 


 





오늘의 이야기

꿀벌 BumbleBee 에서 다시 Chipmunk 다람쥐 버전으로 upgrade 을 진행 했고, 다시 dolpin 돌고래 로 나아가기를 하고 있는 것 같다. 


 


개발자 입장에서는 안정화된 버전인 BumbleBee 버전을 사용하는 것이 좋은 것 같기는 하나, 뭐 어떤 가 얼리아답터 처럼 미리 접해 보는 것도 나쁘지 않을 것 같아서 오늘은 귀여운(?) Chipmunk 다람쥐 에 대해서 살펴 보고자 한다. 


chipmunk 다람쥐 버전 이미지



 


dolphin 이미지



 


하지만 아직 오류가 많아 보인다.  개발자 입장에서는 안정된 버전으로 작업을 하는 게 좋은 거지... 시작 하자 마자 오류가 나는 건 사용하기가 아직 이르다...


 


IDE 에러 메시지



 


plugin 때문에 어떤 오류가 ...



알 수 없다... 아직은...


 


일단 아쉬운 데로 Disable plugin 을 클릭해서 NDK 사용을 하지 않는 것으로 했더니 정리가 된 것 같기는 하다. 아직 NDK사용이 뭔지 모르는 나에게 그만...


 


다음은 Chipmuck 버전에서 나오는 변화된 것에 대한 내용 인데 , 


 


Gradle Managed Virtual Devices


자동화된 계측 테스트에 Android 가상 장치를 사용할 때 일관성, 성능 및 안정성을 개선하기 위해 Gradle 관리 가상 장치를 도입합니다. 이 기능을 사용하면 빌드 시스템이 자동화된 테스트를 실행하기 위해 해당 장치를 완전히 관리(즉, 생성, 배포 및 해체)하는 데 사용하는 프로젝트의 Gradle 파일에서 가상 테스트 장치를 구성할 수 있습니다.
 
모듈 수준 build.gradle 파일에서 Gradle이 앱 테스트에 사용할 가상 기기를 지정할 수 있습니다. 다음 코드 샘플은 API 레벨 29를 실행하는 Pixel 2를 Gradle 관리 기기로 생성합니다.


 


android {
testOptions {
devices {
pixel2api29 (com.android.build.api.dsl.ManagedVirtualDevice) {
// Use device profiles you typically see in
// Android Studio
device = "Pixel 2"
apiLevel = 29
// You can also specify "aosp" if you don't require
// Google Play Services.
systemImageSource = "google"
abi = "x86"
}
}
}
}

구성한 Gradle 관리 기기를 사용하여 테스트를 실행하려면 다음 명령어를 사용하세요.


 


gradlew
pixel2api29DebugAndroidTest

 


Automated Test Deices


 


Gradle 관리 장치는 계측 테스트를 실행할 때 CPU 및 메모리 리소스를 줄이도록 최적화된 ATD(자동 테스트 장치)라는 새로운 유형의 에뮬레이터 장치를 지원합니다.
 
Gradle 관리 장치와 함께 ATD 이미지를 사용하려면 아래와 같이 "atd" 이미지를 지정합니다.


android {
testOptions {
devices {
pixel2api29 (com.android.build.api.dsl.ManagedVirtualDevice) {
// Use device profiles you typically see in Android Studio
device = "Pixel 2"
// ATDs currently support only API level 30.
apiLevel = 30
// You can also specify "google-atd"
// if you require Google Play Services.
systemImageSource = "aosp-atd"
abi = "x86"
}
}
}
}

Run tests faster with Test Sharding




Gradle Managed Devices를 사용하여 테스트를 실행할 때 이제 테스트 샤딩을 활성화할 수 있습니다. 이를 통해 병렬로 실행되는 샤드라고 하는 여러 동일한 가상 장치 인스턴스에 테스트 스위트의 테스트를 배포할 수 있습니다. 테스트 샤딩을 활용하면 자동 테스트 장치를 사용하여 완화할 수 있는 추가 계산 리소스 비용으로 전체 테스트 실행 시간을 줄이는 데 도움이 될 수 있습니다. 주어진 테스트 실행에서 사용하려는 샤드 수를 설정하려면 gradle.properties 파일에서 다음을 설정합니다.



android.experimental.androidTest.numManagedDeviceShards=

 


미리보기 download 는 여기서 



 


Android Studio Preview  |  Android Developers


Get early access to the latest features and improvements in Android Studio.


developer.android.com




 


이만 새로운 android studio 에 대해서 살펴 보았다.


 





오늘의 이야기


#스하리1000명프로젝트,
A volte è difficile parlare con i lavoratori stranieri, vero?
Ho realizzato una semplice app che aiuta! Scrivi nella tua lingua e gli altri lo vedono nella loro.
Si traduce automaticamente in base alle impostazioni.
Super pratico per chat facili. Dai un'occhiata quando ne hai la possibilità!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

https://medium.com/@umairkhalid786/splash-screen-api-android-701cfaaf7b70



 


Splash Screen API android


Hey folks, I hope you are doing great, and writing beautiful android apps.


medium.com




오늘은 medium 에서 만난 글 하나를 소개 하고자 한다. splash screen api 에 대한 부분이다. 예전에는 splash 화면을 만들기 위해서 acitivy 하나를 추가해서 만들고 해당 activity 을 실행하게 해서 구현했던 기억이 있다. 


 


https://billcorea.tistory.com/45



 


안드로이드 앱 로딩 페이지 (Splash) 하나 쯤 만들기...


앱을 만들다 보니 로딩 화면에서 광고문구등을 넣어서 사용하고 싶은 요청이 있다. 이런건 어떻게 ? 그냥 쉬운 생각으로 빈 activity 을 만들어서 잠시 보여주고 그냥 닫아 주면 되지 않을까 ? 그래


billcorea.tistory.com




 


이때는 이렇게 만들었는 데 말이다.  android 가 12로 올라가면서 api 가 추가 되었다.  저 위에 글쓴이의 말은 이해가 될 것 같기도 하다가. 그렇지 않기도 해서 여기 저기 찾다가 구현을 해 보았다. 


 


먼저. gradle 에 추가할 부분은 다음과 같이


dependencies {

....

implementation 'androidx.core:core-splashscreen:1.0.0-beta01'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'

.....
}

viewmodel 은 왜 들어가야 하는 가 ? 그것 아직 잘 이해가 되지는 않았다. 뭐 그래도 필요한 듯 하여...


 


다음은 splah 화면으로 사용할 theme 을 추가 한다. res / values 폴더에 splash_theme.xml 로 


<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<!-- I will rather have a splash screen with animated drawable icon
<item name="windowSplashScreenBackground">@color/purple_200</item>
-->
<item name="windowSplashScreenAnimatedIcon">@drawable/dice_1</item>
<item name="windowSplashScreenAnimationDuration">100</item>
<item name="postSplashScreenTheme">@style/Theme.KotlinExam0115</item>
</style>
</resources>

여기서 볼껀


windowSplashScreenBackground 을 사용하면 배경색 지정이 된다는 것이다. 


windowSplashScreenAnimatedIcon 을 이미지 아이콘을 지정하는 것이다. 저기서 지정한 dice_1 은 샘플 코딩 하다가 만든 png 파일이다. 


windowSplashScreenAnimationDuration 은 지속시간을 말하는 것이고


postSplashScreenTheme 는 내 앱 theme 을 지정해 주었다. (ex : Theme.KotlinExample0115 는 내가 만든 앱의 기본 style theme 명칭임)


 


다음은 acitivy 의 구현 부분 인데...


import android.animation.ObjectAnimator
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.animation.AnticipateInterpolator
import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import kotlin.concurrent.thread

class MainActivity : AppCompatActivity() {

var TAG:String = "MainActivity"

override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

splashScreen.apply {
setOnExitAnimationListener { sp ->
sp.iconView.animate().rotation(180F).duration = 3000L
val slideUp = ObjectAnimator.ofFloat(
sp.iconView,
View.TRANSLATION_Y,
0f,
-sp.iconView.height.toFloat()
)
slideUp.interpolator = AnticipateInterpolator()
slideUp.doOnEnd {
sp.remove()
}
slideUp.start()
}
}
}
}

이 정도의 구현... 먼저 splashScreen 을 선언해 주고. 아래 에서 일정시간 동안 동작하고 꺼지는 화면을 구현 하도록 만들어 주는 정도가 되면 될 것 같다.   안드로이드 개발자 가이드는 아래 링크를 참고 하였다.


 


https://developer.android.com/guide/topics/ui/splash-screen



 


Splash screens  |  Android Developers


Splash screens Important: If you have previously implemented a custom splash screen in Android 11 or lower, you'll need to migrate your app to the SplashScreen API to ensure that it displays correctly in Android 12 and higher. For instructions, see Migra


developer.android.com




 


 



실행영상


 


실행되는 영상 이미지는 이렇게 동작을 한다.  저 주사위 이미지가 나왔다가. 사라지는 모양으로... 응용해 보면 되지 않을 가 ?


 


splash screen sample



 


이것으로 오늘은 정리 끝.





오늘의 이야기

오늘은 admob 에서 온 메일 이야기를 한가지 해 둘까 한다.  아마도 앱에 달리는 광고 때문일 것 같다.


 











제목 : Google Play 및 Android 변경사항을 지원하기 위한 핵심 GMA SDK 업데이트
Google Play 및 Android 변경사항을 지원하기 위한 권장 업데이트

 


메일 본문



구글의 보안정책이 강화 되면서 광고 게시와 관련된 것들도 강화 되고 있는 것 같은 생각이 든다. 앞으로는 앱에 광고판 달아서 수입을 얻기에는 어려운 시간이 될 것 같은...


 


아직 잘 모르는 부분이라서 오늘 부터는 적어두고 찾아 보려고 한다. 


 


1. 타겟팅


 


https://developers.google.com/admob/android/targeting#child-directed_setting



 


타겟팅  |  Android  |  Google Developers


타겟팅 This guide explains how to provide targeting information to an ad request. For a working example, download the Android API Demo app. Download API Demo Prerequisite RequestConfiguration RequestConfiguration is an object that collects targeting inf


developers.google.com




먼저 찾은 건 앱 사용자의 범위에 따라서 광고 대상 범위를 정하는 문제 일 것 같다.  어린이 대상인 앱일 때...  내가 만들고 있는 앱들은 그런 경우가 없지만, 혹시나... 전연령으로 배포가 가능한 앱일 경우... 위 링크의 타켓팅 설정을 하지 않을 때, 아마도 광고 제한이 있을 것으로 생각이 된다. 


 


TAG_FOR_CHILD_DIRECTED_TREATMENT_TRUE  그래서 아동용일때 설정은 이렇게 해 두어야 하고... 아닐 때는 FALSE 을 선택해야 하고...


 


 


2. SDK 버전 확인 


 


https://support.google.com/admob/answer/11402075



 


Google Play 및 Android 변경사항에 맞게 앱 준비하기 - Google AdMob 고객센터


도움이 되었나요? 어떻게 하면 개선할 수 있을까요? 예아니요


support.google.com




implementation 'com.google.android.gms:play-services-ads:20.5.0'

광고 빌드 버전은 20.5.0 이상을 지원해야 한다고 ... 난 이미 그렇게 하고 있으니... 일단 pass ?


 


 


 


3. 구글 가이드 따라 정리 ?


 


-- 아래는 복사한 글 ---


 


https://support.google.com/admob/answer/11402075



 


Google Play 및 Android 변경사항에 맞게 앱 준비하기 - Google AdMob 고객센터


도움이 되었나요? 어떻게 하면 개선할 수 있을까요? 예아니요


support.google.com




Google Play 및 Android 변경사항에 맞게 앱 준비하기






최근에 Google Play와 Android에서 몇 가지 새로운 정책 업데이트 사항과 기술 관련 변경사항을 발표했습니다. 다음 권장사항을 검토하여 앱이 최신 상태이고 규정을 준수하는지 확인하세요. 


Google Play의 데이터 보안 섹션 정보 제공


2021년 5월에 Google Play에서는 개발자가 앱의 사용자 데이터 수집, 공유 및 보안 관행을 공개하는 새로운 데이터 보안 섹션을 발표했습니다. 


조만간 Play Console에서 양식을 작성하여 내 앱의 개인 정보 보호 및 보안 관행에 대해 Google Play에 알려야 합니다. 이 정보는 나중에 Google Play 사용자가 내 앱을 다운로드하기 전에 사용자 데이터의 수집 및 공유 방식을 파악할 수 있도록 스토어 등록정보에 표시됩니다.


Google Play의 데이터 보안 섹션 관련 내용과 Google Play의 데이터 공개 요건에 대비하는 방법에 대해 자세히 알아보세요.


새 앱 세트 ID를 지원하도록 SDK 업데이트 


Android 12 기기부터 사용자가 Android 설정에서 맞춤설정을 선택 해제하면 Google Play에서 광고 ID를 삭제합니다. 또한 Google Play에서는 동일한 조직이 소유한 여러 앱에 걸쳐 개인 정보가 보호되는 방식으로 사용량과 행동을 상호 연관시켜주는 앱 세트 ID를 도입했습니다. 


맞춤설정을 선택 해제한 사용자에게 보고 및 사기 방지 기능을 지원하려면 새 앱 세트 ID를 지원하는 Google 모바일 광고(GMA) SDK 20.5.0 이상으로 업데이트하시기 바랍니다.


광고 ID 권한을 위한 앱 업데이트


대상 API 수준을 31(Android 12)로 업데이트하는 앱에서 광고 ID를 사용하려면 Android 매니페스트 파일에서 Google Play 서비스 일반 권한을 선언해야 합니다. 이 권한을 선언하지 않고 Android 12를 타겟팅하는 앱의 경우 광고 ID가 자동으로 삭제되고 0으로 구성된 문자열로 대체됩니다. 


GMA SDK 20.4.0 이상으로 앱을 업데이트하면 자동으로 권한이 선언됩니다. GMA SDK 20.4.0 이상을 사용 중이면 별도의 조치를 취하지 않아도 광고 ID를 계속 사용할 수 있습니다. 


사용 중지 방법 등 새 권한에 대해 자세히 알아보려면 Play Console 고객센터를 참고하세요.


Google Play 가족 정책의 업데이트 내용 검토


Google Play에서 가족 정책 요건에 대한 업데이트 내용을 발표했습니다. 타겟층에 아동이 포함되는 앱의 경우 아동 또는 연령을 알 수 없는 사용자의 일부 식별자(광고 ID 포함)를 전송하면 안 됩니다.


2022년 초에는 아동 대상 서비스로 취급하기 위해 tagForChildDirected (TFCD) 또는 tagForunderAgeOfConsent (TFUA)를 통해 광고 요청에 태그가 지정된 경우 광고 ID가 전송되지 않게 하는 새로운 GMA SDK 버전을 출시할 예정입니다.


앱의 타겟층에 아동이 포함된 경우 GMA SDK를 2022년 초에 출시되는 최신 버전으로 업데이트하시기 바랍니다. 


광고 ID 권한이 내 앱에 병합되지 않게 하여 앱 전체의 광고 ID를 사용 중지할 수도 있습니다.


앱 사용자의 연령대가 다양하면 아동과 성인을 구분하기 위해 중립적인 연령 심사를 해야 할 수도 있습니다. Google에서는 2022년 초에 AdMob의 개인 정보 보호 및 메시지 도구의 일부로 중립적인 연령 심사 메시지를 출시할 예정입니다.


 


--- 복사한 글 끝 ---


 


 


아직은 잘 이해가 되지 않았다.  추가로 뭔가를 알게 되면 수정해 보도록 하겠다.


 


 









오늘의 이야기

최종 추천 번호: 추천 [16,27,30,31,38,39] 추천 [02,04,18,29,33,40] 추천 [03,11,17,25,32,45] 추천 [07,15,24,28,34,42] 추천 [09,19,26,35,41,43] ### 다음 라운드 번호 추...