반응형

완성코드

public class SecurityManager {

    private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
    private static final String ALIAS = "yourSecretKey";//키값 숨겨야함..
    private static final String TAG = "[SecurityManager]";
    private static String crypto = "RSA/ECB/PKCS1Padding";


    private static SecurityManager sInstance;
    public static SecurityManager getInstance(Context context) {
        if (sInstance == null) {
           sInstance = new SecurityManager(context.getApplicationContext());
        } return sInstance;
    }
    private Context mContext;
    private SecurityManager(Context context) {
        mContext = context;
    }

    private KeyStore.Entry createKeys() throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableEntryException, NoSuchProviderException, InvalidAlgorithmParameterException {
        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
        keyStore.load(null);

        if (!keyStore.containsAlias(ALIAS)) {
            //키스토어 접근
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", ANDROID_KEY_STORE);
//            KeyPairGenerator kpg = KeyPairGenerator.getInstance("AES", "AndroidKeyStore");
            Calendar start = Calendar.getInstance(Locale.ENGLISH);
            Calendar end = Calendar.getInstance(Locale.ENGLISH);
            end.add(Calendar.YEAR, 25);

            //KeyPairGeneratorSpec deprecated
            //mContext.getApplicationContext() -> service , acirivity등을 벗어낫을때 context가 의미가 없어질 수 있음
            KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(mContext.getApplicationContext())
                    .setAlias(ALIAS)
                    .setKeySize(4096)// 512byte
                    .setSubject(new X500Principal("CN=" + ALIAS))
                    .setSerialNumber(BigInteger.valueOf(123456))
                    .setStartDate(start.getTime())
                    .setEndDate(end.getTime())
                    .build();

            kpg.initialize(spec);
            kpg.generateKeyPair();
        }

        return keyStore.getEntry(ALIAS, null);
    }


    private static byte[] encryptUsingKey(PublicKey publicKey, byte[] bytes)  throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        Cipher inCipher = Cipher.getInstance(crypto);
        inCipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return inCipher.doFinal(bytes);
    }

    private static byte[] decryptUsingKey(PrivateKey privateKey, byte[] bytes) throws NoSuchAlgorithmException,  NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException{
        Cipher inCipher = Cipher.getInstance(crypto);
        inCipher.init(Cipher.DECRYPT_MODE, privateKey);
        return inCipher.doFinal(bytes);
    }


    public String encrypt(String plainText) throws NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableEntryException, NoSuchProviderException, InvalidAlgorithmParameterException {
        KeyStore.Entry entry = createKeys();
        if (entry instanceof KeyStore.PrivateKeyEntry) {
            Certificate certificate = ((KeyStore.PrivateKeyEntry) entry).getCertificate();
            PublicKey publicKey = certificate.getPublicKey();
            byte[] bytes = plainText.getBytes("UTF-8");
            byte[] encryptedBytes = encryptUsingKey(publicKey, bytes);
            byte[] base64encryptedBytes = Base64.encode(encryptedBytes, Base64.DEFAULT);
            return new String(base64encryptedBytes);
        }
        return null;
    }
    public String decrypt(String cipherText) throws NoSuchAlgorithmException,  NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, KeyStoreException, CertificateException, IOException, UnrecoverableEntryException {
        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
        keyStore.load(null);

        KeyStore.Entry entry = keyStore.getEntry(ALIAS, null);
        if (entry instanceof KeyStore.PrivateKeyEntry) {
            PrivateKey privateKey = ((KeyStore.PrivateKeyEntry) entry).getPrivateKey();
            byte[] bytes = cipherText.getBytes("UTF-8");
            byte[] base64encryptedBytes = Base64.decode(bytes, Base64.DEFAULT);
            byte[] decryptedBytes = decryptUsingKey(privateKey, base64encryptedBytes);
            return new String(decryptedBytes);
        }
        return null;
    }

}

 

사용

String cipherText  = SecurityManager.getInstance(getContext()).encrypt("암호화 문구");
String decryptText = SecurityManager.getInstance(getContext()).decrypt(cipherText));

 

 


뜯어보기

    private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
    private static final String ALIAS = "yourSecretKey";//키값 숨겨야함..
    private static final String TAG = "[SecurityManager]";
    private static String crypto = "RSA/ECB/PKCS1Padding";

주요하게 봐야할 부분은 ALIAS와 crypto부분입니다.

 

ALIAS

     암호화 키를 생성할때 중요한 암호화 값입니다. 자신만 아는곳에 값을 숨겨서 키를 생성해야합니다.

 

crypto

     RSA / ECB / PKCS1Padding 방식을 채택해 암호화합니다 Cipher객체를 생성할때 사용합니다.

 

RSA암호화 방식 : https://namu.wiki/w/RSA%20%EC%95%94%ED%98%B8%ED%99%94

 

RSA 암호화 - 나무위키

RSA 방식으로 암호화를 하기 위해선 먼저 키를 만들어야 한다. 그 과정은 다음과 같다. 두 소수 p,q p , q p,q 를 준비한다.[4]p−1, q−1 p - 1,\ q - 1 p−1, q−1과 각각 서로소인 정수 eee[5]를 준비한다.[6]ededed를 (p−1)(q−1)(p - 1)(q - 1)(p−1)(q−1)으로 나눈 나머지가 1이 되도록 하는 ddd[7]를 찾는다.[8][9]N=pqN = pqN=pq를 계산한 후, NNN와 eee를 공개한

namu.wiki

 

ECB 모드 : 블록형식으로 암호화하는 방법입니다.

 

pkcs1 padding : 데이터를 블럭으로 암호화 할때 평문이 항상 블럭 크기(일반적으로 64비트 또는 128비트)의 배수가 되지 않는다. 패딩은 어떻게 평문의 마지막 블록이 암호화 되기 전에 데이터로 채워지는가를 확실히 지정하는 방법이다. 복호화 과정에서는 패딩을 제거하고 평문의 실제 길이를 지정하게 된다.

 

 

 private KeyStore.Entry createKeys() throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableEntryException, NoSuchProviderException, InvalidAlgorithmParameterException {
        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
        keyStore.load(null);

        if (!keyStore.containsAlias(ALIAS)) {
            //키스토어 접근
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", ANDROID_KEY_STORE);
//            KeyPairGenerator kpg = KeyPairGenerator.getInstance("AES", "AndroidKeyStore");
            Calendar start = Calendar.getInstance(Locale.ENGLISH);
            Calendar end = Calendar.getInstance(Locale.ENGLISH);
            end.add(Calendar.YEAR, 25);

            //KeyPairGeneratorSpec deprecated
            //mContext.getApplicationContext() -> service , acirivity등을 벗어낫을때 context가 의미가 없어질 수 있음
            KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(mContext.getApplicationContext())
                    .setAlias(ALIAS)
                    .setKeySize(4096)// 512byte
                    .setSubject(new X500Principal("CN=" + ALIAS))
                    .setSerialNumber(BigInteger.valueOf(123456))
                    .setStartDate(start.getTime())
                    .setEndDate(end.getTime())
                    .build();

            kpg.initialize(spec);
            kpg.generateKeyPair();
        }

        return keyStore.getEntry(ALIAS, null);
    }

 

-  기본 설정햇을땐 256 바이트가 넘으면 오류가 낫다. 

   .setKeySize(4096)// 512byte 

    키 사이즈를 늘려 더 긴 택스트를 수용하도록 수정하였다.

 

   * RSA 암호화 방식은 키값보다 암호화하려는 text의 바이트값이 크면 암호화 할수 없다.

 

 

 

 

참고 : https://android.jlelse.eu/storing-data-securely-on-android-keystore-asymmetric-83b1dc5f47db

 

Storing data securely on Android-KeyStore Asymmetric

Saving data securely on Android devices is one of the most challenging problem. I will investigate some methods with their pros and cons.

android.jlelse.eu

 

반응형
반응형

결제를 위한 클래스 만들기

import UIKit
import StoreKit

//MARK: -type alias
public typealias ProductIdentifier = String
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> Void

extension Notification.Name {
    static let IAPHelperPurchaseNotification = Notification.Name("IAPHelperPurchaseNotification")
}
//MARK: -class
open class InAppPurchaseManager: NSObject  {
    let TAG = "[custom][yourTAG][InAppPurchaseManager]"
    static let subID: String = "com.your.custom.subscriptionID"
    //상품 비밀번호
    static let sharePS: String = "aaaaaaaaaaaaaaaaaaaaaaa"
    
    //불러올 상품의 아이디
    private var purchasedPID: Set<ProductIdentifier> = []
    //SKProductsRequestDelegate의 delegate를 갖기위함.
    private var productsRequest: SKProductsRequest?
    //제품을 불러온뒤 controller에 알려주기위한 callback
    private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
    //옵저버 등록후 상품리스트를 불러오면 바로 complete가 되서 결제됨을 막음....
    private var didTheListComeOver = false
    
    public init( productIdentifiers:Set<ProductIdentifier> ){
        self.purchasedPID = productIdentifiers
        super.init()
        //appStore 결제 모듈에 연결함..
        SKPaymentQueue.default().add(self)
    }
    //상품 리스트를 요청
    // productsRequestCompletionHandler로 메인으로 전달, ui update
    public func requestProducts(completionHandler: @escaping ProductsRequestCompletionHandler) {
      productsRequest?.cancel()
      productsRequestCompletionHandler = completionHandler

      productsRequest = SKProductsRequest(productIdentifiers: purchasedPID)
      productsRequest!.delegate = self
      productsRequest!.start()
    }
    
    //ui로 보여진뒤 구입을 누르면 구입 프로세스 진행
    public func buyProduct(_ product: SKProduct){
        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    }
    //이미 구입했는지 여부 확인 (여기서 안씀..)
    public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
        return purchasedPID.contains(productIdentifier)
    }
    
    //결제가 가능한 폰,계정상태 리턴
    public class func canPayments() -> Bool {
        return SKPaymentQueue.canMakePayments()
    }
 
}
//MARK: - get product delegate
extension InAppPurchaseManager:SKProductsRequestDelegate{
    
    //success get products
    public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        logi("\(TAG) Loaded list of products...")
        let products = response.products
        productsRequestCompletionHandler?(true, products)//get product complete
        clearRequestAndHandler()
        didTheListComeOver = true
    }
    
    //get products error!
    public func request(_ request: SKRequest, didFailWithError error: Error) {
      loge("\(TAG) didFailWithError : Failed to load list of products.")
      loge("\(TAG) didFailWithError :  \(error.localizedDescription)")
      productsRequestCompletionHandler?(false, nil)
      clearRequestAndHandler()
    }

    private func clearRequestAndHandler() {
      productsRequest = nil
      productsRequestCompletionHandler = nil
    }
}

//MARK: - Payment Transction
extension InAppPurchaseManager: SKPaymentTransactionObserver {
 
  //앱스토어 결제 모듈에 연결된 부분.. queue가 계속 연결되어있음
  public func paymentQueue(_ queue: SKPaymentQueue,updatedTransactions transactions: [SKPaymentTransaction]) {
     loge("\(TAG) paymentQueue ----->>")
    if (didTheListComeOver){
        for transaction in transactions {
          switch transaction.transactionState {
          case .purchased:
            complete(transaction: transaction)
            break
          case .failed:
            fail(transaction: transaction)
            break
          case .restored:
            restore(transaction: transaction)
            break
          case .deferred:
            break
          case .purchasing:
            break
          }
        }
    }
  }
 //결제성공
  private func complete(transaction: SKPaymentTransaction) {
    logi("\(TAG) complete...")
    deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
    
    SKPaymentQueue.default().finishTransaction(transaction)
    didTheListComeOver = false
    NotificationCenter.default.post(name: .completePurchase, object: nil, userInfo: ["complete": "결제 성공"])
  }
 
  //이미 결제된 사항을 복원
  private func restore(transaction: SKPaymentTransaction) {
    guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
 
     logi("\(TAG) restore... \(productIdentifier)")
    deliverPurchaseNotificationFor(identifier: productIdentifier)
    SKPaymentQueue.default().finishTransaction(transaction)
    didTheListComeOver = false
    NotificationCenter.default.post(name: .completePurchase, object: nil, userInfo: ["complete": "결제 성공(재구입)"])
  }
 
  //실패
  private func fail(transaction: SKPaymentTransaction) {
     logi("\(TAG) SKPaymentTransaction  fail...")
    if let transactionError = transaction.error as NSError?,
      let localizedDescription = transaction.error?.localizedDescription,
        transactionError.code != SKError.paymentCancelled.rawValue {
        print("Transaction Error: \(localizedDescription)")
      }

    SKPaymentQueue.default().finishTransaction(transaction)
    didTheListComeOver = false
    NotificationCenter.default.post(name: .cancelPurchase, object: nil, userInfo: ["fail": "결제 실패"])
  }
 
  private func deliverPurchaseNotificationFor(identifier: String?) {
    guard let identifier = identifier else { return }
 
    purchasedPID.insert(identifier)
    UserDefaults.standard.set(true, forKey: identifier)
    NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
  }
}

 


결제 사용

// 인앱결제 구조체
public struct InAppProducts {
    public static let product = "com.your.app.subscription_id"
    public static let productK: String = "aaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    public static let productIdentifiers: Set<ProductIdentifier> = [InAppProducts.product]
    public static let store2 = InAppPurchaseManager(productIdentifiers: InAppProducts.productIdentifiers)
}

 

    func purchaseShow(){
        if InAppPurchaseManager.canPayments() {
        //상품을 불러오는 부분
            InAppProducts.store2.requestProducts { (isCheck, products) in
                for p in products!{
                    if isCheck {
                       //상품 불러온후 결제 프로세스 진행
                        InAppProducts.store2.buyProduct(p)
                    }
                }
            }
        } else {
            //결제설정이 안되었거나 결제가 불가능한 기기입니다
        }
    }

결제체크용 클래스


import UIKit
import StoreKit

public typealias PurchaseCheckHandler = (_ purchased: Bool, _ msg:String?) -> Void

struct ReceiptParam {
    public static let data :String = "receipt-data"
    public static let password :String = "password"
    public static let excludeOld_trsc :String = "exclude-old-transactions"
}

open class InAppPurchaseChecker: NSObject {
    
    let TAG = "[custom][yourTAG][InAppPurchaseManager]"
    var productID:String
    var productKey:String
    
    //결제 상태를 controller에 돌려주기위한 handler
    private var checkHandler: PurchaseCheckHandler?
    
    
    public init(productID:String, productKey:String) {
        self.productID = productID
        self.productKey = productKey
        super.init()
    }
    
    func checkPurchase(purchaseCheckHandler: @escaping PurchaseCheckHandler ){
        self.checkHandler = purchaseCheckHandler
        refreshSubscriptionsStatus()
    }
    
    
//앱스토어에서 영수증 가저오는 부분
private func refreshSubscriptionsStatus(){
    logi("\(self.TAG) check refreshSubscriptionsStatus")
    guard let receiptUrl = Bundle.main.appStoreReceiptURL else {
            refreshReceipt()
            return
    }
    
    logi("\(TAG) \(receiptUrl)")
    
    let urlString = "https://sandbox.itunes.apple.com/verifyReceipt"
//      let urlString = "https://buy.itunes.apple.com/verifyReceipt"

    let receiptData = try? Data(contentsOf: receiptUrl).base64EncodedString()
    let requestData = [ReceiptParam.data : receiptData ?? "", ReceiptParam.password : productKey ,ReceiptParam.excludeOld_trsc : true] as [String : Any]
    var request = URLRequest(url: URL(string: urlString)!)
    request.httpMethod = "POST"
    request.setValue("Application/json", forHTTPHeaderField: "Content-Type")
    let httpBody = try? JSONSerialization.data(withJSONObject: requestData, options: [])
    request.httpBody = httpBody
    URLSession.shared.dataTask(with: request)  { (data, response, error) in
        DispatchQueue.main.async {
            if data != nil {
                logi("\(self.TAG) come data")
                if let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments){
                    self.parseReceipt(json as! Dictionary<String, Any>)
                      logi("\(self.TAG) will do parseReceipt")
                    return
                }
            } else {
                loge("\(self.TAG)error validating receipt: \(error?.localizedDescription ?? "")")
            }
        }
    }.resume()
     
}
//못불러왔을경우 --> 영수증 리프래쉬
private func refreshReceipt(){
    let request = SKReceiptRefreshRequest(receiptProperties: nil)
    request.delegate = self
    request.start()
}
// json 으로 받아온 영수증 파싱해서 controller에 결과값 전송
private func parseReceipt(_ json : Dictionary<String, Any>) {
    //가저온 결과값중 가장 마지막 아이템 가저오기...
    guard let receipts_array = json["latest_receipt_info"] as? [Dictionary<String, Any>] else {
        checkHandler!(false,"애플 정기결제를 한적이 없습니다.")
        return
    }
    
    for receipt in receipts_array {
        let pid = receipt[IAPVO.product_id] as! String
        if pid == productID {
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"
            formatter.timeZone = NSTimeZone(name: "UTC") as TimeZone?
            if let date = formatter.date(from: receipt[IAPVO.expires_date] as! String) {
                logi("\(self.TAG) date: \(date), Date(): \(Date())" )
                //만료일자가 오늘보다 커야 결제한 상태이다.
                if date > Date() {
                    logi("\(self.TAG) date > Date()")
                    DispatchQueue.main.async {
                        self.checkHandler!(true,"결제한 상태")
                    }
                }
                //결제 취소상태
                else {
                    logi("\(self.TAG) date < Date()")
                    DispatchQueue.main.async {
                        self.checkHandler!(true,"결제한 취소한 상태")
                    }

                }
            }
        }
    }
}
    
    
    
    
}

extension InAppPurchaseChecker : SKRequestDelegate {
    public func request(_ request: SKRequest, didFailWithError error: Error) {
         loge("\(self.TAG) didFailWithError !!")
    }
    public func requestDidFinish(_ request: SKRequest) {
         logi("\(self.TAG) requestDidFinish !!")
    }
}

 


결제 체크 사용

let iapChecker = InAppPurchaseChecker(productID: InAppProducts.product, 
		productKey: InAppProducts.productK)
iapChecker.checkPurchase { (isPurchased, msg) in
	if isPurchased {
		// Purchased!! , update your ui
	} else {
		// not purchased!! ,update your ui
	}
}
반응형
반응형

1편 : 기본적인 recycle view 구현

2편 : viewModel을 이용한 recycle view 구현

3편 : diffUtil callback을 이용한 recycle view 구현

 

차례대로 개시할 생각입니다.

 

뷰구성

기본적인 recycle view를 만들어줍니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/demo_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:paddingTop="@dimen/spacing_small"
    android:paddingBottom="@dimen/spacing_small"
    android:scrollbarStyle="outsideOverlay"
    android:scrollbars="vertical"
    android:layoutAnimation="@anim/grid_layout"/>

-여기서 layoutAnimation는 recycleview 각자 아이템들의 에니메이션을 담당합니다.

 

 

recycle view를 inflater를 이용해 불러와줍니다.

class DefaultListFragment :Fragment(){


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        return inflater.inflate(R.layout.default_list_fragment, container, false)
    }
    
}

 

adapter 생성

기본적인 adapter 생성

class DefaultListAdapter(private val context: Context,
private val model:ArrayList<DefaultListModel>,
private val onListClick:(model:DefaultListModel) -> Unit )
	:RecyclerView.Adapter<DefaultListAdapter.DefaultListHolder>() {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DefaultListHolder {
       
    }

    override fun getItemCount(): Int {
       
    }

    override fun onBindViewHolder(holder: DefaultListHolder, position: Int) {
       
    }


    inner class DefaultListHolder(parent: ViewGroup) : RecyclerView.ViewHolder(

    ) {
    
    }
}

- 어댑터에서 사용할 기본적인 변수들을 받습니다.

- context : Glide를 이용해 이미지를 받아오기오려면 context객체가 필요하기때문에 받아왔습니다.

- model : 데이터를 저정하고있는 list를 받아왔습니다.

- onListClick : 클릭했을때 메인에서 변수를 넘겨받을수 있도록 클릭 리스너를 받아옵니다.

 

 

 

각 아이템에 쓰일 layout xml 생성

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
    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/demo"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="@dimen/spacing_small"
    android:layout_marginTop="@dimen/spacing_tiny"
    android:layout_marginRight="@dimen/spacing_small"
    android:layout_marginBottom="@dimen/spacing_tiny">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageview"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:src="@drawable/no_image"
        android:scaleType="fitXY"
        app:layout_constraintBottom_toTopOf="@id/title_label"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

        <TextView
            android:id="@+id/title_label"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/spacing_medium"
            android:layout_marginTop="@dimen/spacing_medium"
            android:layout_marginRight="@dimen/spacing_medium"
            android:gravity="start|center_vertical"
            android:textAppearance="?attr/textAppearanceHeadline5"
            app:layout_constraintBottom_toTopOf="@+id/body_label"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/imageview"
            tools:text="Hello, world!" />

        <TextView
            android:id="@+id/body_label"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/spacing_medium"
            android:gravity="start|center_vertical"
            android:textAppearance="?attr/textAppearanceListItemSecondary"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/title_label"
            tools:text="Hello, world!" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

 

holder에 연결

    inner class DefaultListHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
		LayoutInflater.from(parent.context).inflate(R.layout.default_list_item, parent, false)
    ) {
        private val iv:ImageView = itemView.findViewById(R.id.imageview)
        private val titleView:TextView = itemView.findViewById(R.id.title_label)
        private val bodyView:TextView = itemView.findViewById(R.id.body_label)

        fun bindView(model:DefaultListModel){
            titleView.text = model.defaultTitle
            bodyView.text = model.defaultBody
            Glide
                .with(context)
                .load( model.defaultImage)
                .centerCrop()
                .placeholder(R.drawable.no_image)
                .into(iv)
        }
    }

- bindView는 onBindViewHolder메소드에서 holder를 가저와 사용할 생각입니다.

 

 

만들어놓은 것들 연결

class DefaultListAdapter(
  private val context: Context,
  private val model:ArrayList<DefaultListModel>,
  private val onListClick:(model:DefaultListModel) -> Unit
  )
  : RecyclerView.Adapter<DefaultListAdapter.DefaultListHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DefaultListHolder {
        return DefaultListHolder(parent).apply {
            itemView.setOnClickListener {
                onListClick(model.get(adapterPosition))
            }
        }
    }

    override fun getItemCount(): Int {
        return model.size
    }

    override fun onBindViewHolder(holder: DefaultListHolder, position: Int) {
        holder.bindView(model.get(position))
    }


    inner class DefaultListHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
        LayoutInflater.from(parent.context).inflate(R.layout.default_list_item, parent, false)
    ) {
        private val iv:ImageView = itemView.findViewById(R.id.imageview)
        private val titleView:TextView = itemView.findViewById(R.id.title_label)
        private val bodyView:TextView = itemView.findViewById(R.id.body_label)

        fun bindView(model:DefaultListModel){
            titleView.text = model.defaultTitle
            bodyView.text = model.defaultBody
            Glide
                .with(context)
                .load( model.defaultImage)
                .centerCrop()
                .placeholder(R.drawable.no_image)
                .into(iv)
        }
    }
}

Main에 연결

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val data = context?.let {
            Parser.getAlbumList(it,"DefaultList")
        }
        val rcv: RecyclerView = view.findViewById(R.id.demo_list)
        val mAdapter = context?.let {
            data?.let { it1 ->
                DefaultListAdapter(it, it1){ list ->
                    Log.d("asdf","Title : ${list.defaultTitle}, Body : ${list.defaultBody}, image : ${list.defaultImage}")
                }
            }
        }

        rcv.apply {
            layoutManager = GridLayoutManager(context,2)
            adapter = mAdapter
        }
    }

- 특별한것은 없고  list 생성후 연결시켜주면된다.

반응형
반응형

서버쪽에서 css소스를 반영했는데 바로 적용이 안되는 문제가 발생해 찾아보니 앱캐시가 문제였다.

 

swift

//원하는 캐시 데이터만 골라서 삭제
let websiteDataTypes = NSSet(array: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache, WKWebsiteDataTypeCookies])
let date = NSDate(timeIntervalSince1970: 0)
WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes as! Set, modifiedSince: date as Date, completionHandler:{
	//remove callback
})

// 모든 열어본 페이지에 대한 데이터를 모두 삭제
WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), completionHandler: {
    (records) -> Void in
    for record in records{
        WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {})
       //remove callback
    }
})

 

 

objective c

    //원하는 캐시데이터 삭제
    NSSet* nSet= [NSSet setWithArray:@[WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache, WKWebsiteDataTypeCookies]];
    NSDate *nDate=[NSDate dateWithTimeIntervalSince1970:0];
    [WKWebsiteDataStore.defaultDataStore removeDataOfTypes:nSet modifiedSince:nDate completionHandler:^{
        //delete callback
    }];
    
    //모든 캐시데이터 삭제
    [WKWebsiteDataStore.defaultDataStore fetchDataRecordsOfTypes:WKWebsiteDataStore.allWebsiteDataTypes completionHandler:^(NSArray<WKWebsiteDataRecord *> * records) {
        for(WKWebsiteDataRecord *record in records){
            [WKWebsiteDataStore.defaultDataStore removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{
                //delete callback
            }];
        }
    }];

 

 

 

위 3개 타입외에도 앱에 들어가있는 아래 타입데이터들도 삭제가 가능하다.

WKWebsiteDataTypeDiskCache,
WKWebsiteDataTypeOfflineWebApplicationCache,
WKWebsiteDataTypeMemoryCache,
WKWebsiteDataTypeLocalStorage,
WKWebsiteDataTypeCookies,
WKWebsiteDataTypeSessionStorage,
WKWebsiteDataTypeIndexedDBDatabases,
WKWebsiteDataTypeWebSQLDatabases,
WKWebsiteDataTypeFetchCache, //(iOS 11.3, *)
WKWebsiteDataTypeServiceWorkerRegistrations, //(iOS 11.3, *)

설명은 아래와같습니다.

WKWebsiteDataTypeCookies

Cookies.

WKWebsiteDataTypeIndexedDBDatabases

IndexedDB databases.

WKWebsiteDataTypeLocalStorage

HTML local storage.

WKWebsiteDataTypeMemoryCache

In-memory caches.

WKWebsiteDataTypeOfflineWebApplicationCache

HTML offline web application caches.

WKWebsiteDataTypeSessionStorage

HTML session storage.

WKWebsiteDataTypeWebSQLDatabases

WebSQL databases.

 

반응형
반응형

 

    private void onWebViewSet() {
        wvMainWebview.addJavascriptInterface(new AndroidBridge(), Common.ANDROID_BRIDGE_NAME);
        wvMainWebview.setWebViewClient(new mMainWebviewClient());
        wvMainWebview.setWebChromeClient(new mMainWebChromeClient());
    }

 

JavascriptInterface 

AndroidBridge이름으로 웹과 통신하는 클래스를 만들어줍니다.

이녀석의 역할은 웹에서 앱으로 문자열을 전달합니다. (문자열을 이용해 원하는 기능을 구현할수있습니다.)

 

private class AndroidBridge {
    @JavascriptInterface
    public void executeMobile(final String arg) {
        mAndroidBridgeHandler.post(new Runnable() {
            @Override
            public void run() {
                //arg를 string type으로 받아와 데이터 처리한다.
            }
        }
    }
}

 

 

 

WebViewClient

페이지가 로드를 시작했을때 , 오류가 났을때, 로드가 완료 했을때 등 ... 현 페이지에 load상태를 알수있다.

 

   private class mMainWebviewClient extends WebViewClient {
        private static final String TAG ="mMainWebviewClient";

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            L.i("shouldOverrideUrlLoading(), url : " + url);

            setUrlNameAndSetLogListener(url);

            if( url.startsWith("http:") || url.startsWith("https:") ) {
                return false;
            }

            if (url.startsWith("tel:")) {
                Intent tel = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
                startActivity(tel);
                return true;
            }

            else if (url.startsWith("mailto:")) {
                String body = "Enter your Question, Enquiry or Feedback below:\n\n";
                Intent mail = new Intent(Intent.ACTION_SEND);
                mail.setType("application/octet-stream");
                mail.putExtra(Intent.EXTRA_EMAIL, new String[]{"email address"});
                mail.putExtra(Intent.EXTRA_SUBJECT, "Subject");
                mail.putExtra(Intent.EXTRA_TEXT, body);
                startActivity(mail);
                return true;
            }

            else if (url.startsWith("intent:")) {
                try {
                    Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    Intent existPackage = getPackageManager().getLaunchIntentForPackage(intent.getPackage());
                    if (existPackage != null) {
                        startActivity(intent);
                    } else {
                        Intent marketIntent = new Intent(Intent.ACTION_VIEW);
                        marketIntent.setData(Uri.parse("market://details?id=" + intent.getPackage()));
                        startActivity(marketIntent);
                    }
                    return true;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }


            return true;
        }

        @TargetApi(Build.VERSION_CODES.M)
        @Override
        public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
            onReceivedError(view, error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString());
        }
        @SuppressWarnings("deprecation")
        @Override
        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
            String message = getResources().getString(R.string.webview_error_unkown);
            switch(errorCode) {
                case ERROR_AUTHENTICATION:            // 서버에서 사용자 인증 실패
                    message = getResources().getString(R.string.webview_error_authentication);
                    break;
                case ERROR_BAD_URL:                    // 잘못된 URL
                    message = getResources().getString(R.string.webview_error_bad_url);
                    break;
                case ERROR_CONNECT:                    // 서버로 연결 실패
                    message = getResources().getString(R.string.webview_error_connect);
                    break;
                case ERROR_FAILED_SSL_HANDSHAKE:     // SSL handshake 수행 실패
                    message = getResources().getString(R.string.webview_error_failed_ssl_handshake);
                    break;
                case ERROR_FILE:                        // 일반 파일 오류
                    message = getResources().getString(R.string.webview_error_file);
                    break;
                case ERROR_FILE_NOT_FOUND:            // 파일을 찾을 수 없습니다
                    message = getResources().getString(R.string.webview_error_file_not_found);
                    break;
                case ERROR_HOST_LOOKUP:               // 서버 또는 프록시 호스트 이름 조회 실패
                    message = getResources().getString(R.string.webview_error_host_lookup);
                    break;
                case ERROR_IO:                         // 서버에서 읽거나 서버로 쓰기 실패
                    message = getResources().getString(R.string.webview_error_io);
                    break;
                case ERROR_PROXY_AUTHENTICATION:    // 프록시에서 사용자 인증 실패
                    message = getResources().getString(R.string.webview_error_proxy_auttentication);
                    break;
                case ERROR_REDIRECT_LOOP:            // 너무 많은 리디렉션
                    message = getResources().getString(R.string.webview_error_redirect_loop);
                    break;
                case ERROR_TIMEOUT:                   // 연결 시간 초과
                    message = getResources().getString(R.string.webview_error_timeout);
                    break;
                case ERROR_TOO_MANY_REQUESTS:       // 페이지 로드중 너무 많은 요청 발생
                    message = getResources().getString(R.string.webview_error_too_many_requests);
                    break;
                case ERROR_UNKNOWN:                   // 일반 오류
                    message = getResources().getString(R.string.webview_error_unkown);
                    break;
                case ERROR_UNSUPPORTED_AUTH_SCHEME: // 지원되지 않는 인증 체계
                    message = getResources().getString(R.string.webview_error_unsupported_auth_scheme);
                    break;
                case ERROR_UNSUPPORTED_SCHEME:       // URI가 지원되지 않는 방식
                    message = getResources().getString(R.string.webview_error_unsupported_scheme);
                    break;
            }
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            super.onPageStarted(view, url, favicon);
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
        }
    }​

string.xml

<string name="webview_error_authentication">서버에서 사용자 인증 실패</string>
<string name="webview_error_bad_url">잘못된 URL</string>
<string name="webview_error_connect">서버로 연결 실패</string>
<string name="webview_error_failed_ssl_handshake">SSL handshake 수행 실패</string>
<string name="webview_error_file">일반 파일 오류</string>
<string name="webview_error_file_not_found">파일을 찾을 수 없습니다</string>
<string name="webview_error_host_lookup">서버 또는 프록시 호스트 이름 조회 실패</string>
<string name="webview_error_io">서버에서 읽거나 서버로 쓰기 실패</string>

<string name="webview_error_proxy_auttentication">프록시에서 사용자 인증 실패</string>
<string name="webview_error_redirect_loop">너무 많은 리디렉션</string>
<string name="webview_error_timeout">연결 시간 초과</string>
<string name="webview_error_too_many_requests">페이지 로드중 너무 많은 요청 발생</string>
<string name="webview_error_unkown">일반 오류</string>
<string name="webview_error_unsupported_auth_scheme">지원되지 않는 인증 체계</string>
<string name="webview_error_unsupported_scheme">URI가 지원되지 않는 방식</string>

WebChromeClient

 

javascript의 기본적인기능인 alert이나 confirm등을 제어해 ui를 입혀줄수있는 녀석이다.

   public class mMainWebChromeClient extends WebChromeClient {
        @Override
        public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
            callback.invoke(origin, true, false);
        }

        @Override
        public void onProgressChanged(WebView view, int progress) {
            L.d("onProgressChanged, progress : " + progress);
            if ( progress == 100 ) {
                showLoading( false );
                mEventHandler.removeMessages( Common.HANDLER_WEBVIEW_CONNECTION_CANCEL );
            }
        }

        @Override
        public boolean onJsAlert(WebView view, String url, String message, final android.webkit.JsResult result) {
            new AlertDialog.Builder(view.getContext())
                    .setTitle(getText(R.string.notice))
                    .setTitle("")
                    .setMessage(message)
                    .setPositiveButton(android.R.string.ok,
                            new AlertDialog.OnClickListener() {
                                public void onClick(DialogInterface dialog, int which) {
                                    result.confirm();
                                }
                            })
                    .setCancelable(false)
                    .create()
                    .show();
            return true;
        }

        @Override
        public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
            new AlertDialog.Builder(view.getContext())
                    .setTitle(getText(R.string.notice))
                    .setTitle("")
                    .setMessage(message)
                    .setPositiveButton(android.R.string.ok,
                            new AlertDialog.OnClickListener(){
                                public void onClick(DialogInterface dialog, int which) {
                                    result.confirm();
                                }
                            })
                    .setNegativeButton(android.R.string.cancel,
                            new AlertDialog.OnClickListener(){
                                public void onClick(DialogInterface dialog, int which) {
                                    result.cancel();
                                }
                            })
                    .setCancelable(false)
                    .create()
                    .show();
            return true;
        }
    }

// 자바 스크립트 사용
webSettings.setjavaScriptEnabled(true);



// mixed content 처리 여부

webSettings.setMixedContentMode(int)

MIXED_CONTENT_ALWAYS_ALLOW

MIXED_CONTENT_COMPATIBILITY_MODE

MIXED_CONTENT_NEVER_ALLOW



// DOM Storage Api 허용 여부

webSettings.setDomStorageEnabled(true);

이 부분이 중요한게 특정 사이트의 경우 메뉴 버튼을 이 API 를 사용하는 경우가 있어 이것을 허용해주지 않으면 버튼을 눌렀는데 반응이 없는 경우가 있다. 

예시 > rakuten.co.jp



// 기본 인코딩 설정

webSettings.setDefaultTextEncodingName("UTF-8");

// 웹 뷰에 맞게 출력

webSettings.setLoadWithOverviewMode(true);

webSettings.setUseWideViewPort(true);



// 플러그인 사용

webSettings.setPluginState(PluginState.ON);



// 화면 줌 컨트롤과 제스처를 사용하여 확대

webSettings.setSupportZoom(true);



// 내장 줌 컨트롤 사용

webSettings.setBuildInZoomControls(true);

// 내장 줌 컨트롤 표시 여부

webSettings.setDisplayZoomControls(false);



// 앱 캐시 사용 여부 설정

webSettings.setAppCacheEnabled(true);

// 앱 캐시 크기 설정

webSettings.setAppCacheMaxsize(1024*1024*8);

// 캐시 파일 경로 설정

webSettings.setAppCachePath(path);

// 캐시 방식 설정

webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);



// 암호 저장

webSettings.setSavepassword(true);



// 양식 데이터 저장

webSettings.setSaveFormData(true);



// 웹 뷰 내에서 파일 액세스 활성화

webSettings.setAllowFileAccess(true); API 3

파일에 접근하는 것을 허용

webSettings.getAllowFileAccessFromFileURLs(true); API 16

파일 구성표 URL의 컨텍스트에서 실행중인 JavaScript가 다른 파일 구성표 URL의 콘텐츠에 액세스 할 수 있는지 여부를 가져옵니다.

webSettings.getAllowUniversalAccessFromFileURLS(true); API 16

파일 구성표 URL의 컨텍스트에서 실행되는 JavaScript가 모든 출처의 콘텐츠에 액세스 할 수 있는지 여부를 가져옵니다. 여기에는 다른 파일 구성표 URL의 내용에 대한 액세스가 포함됩니다.



// 마우스 오버를 활성화 하기 위해 Light Touch를 사용 여부 설정

webSettings.setLighttouchEnabled(true);
반응형
반응형

단순 interface만 사용하면 재미없을거같아서 요즘 안드로이드에서 많이 보이는 interface스타일로 ADID를 가저와보려고 합니다.

시작하기전에 구글에 ADID에대한 설명을 간략하게 읽고 시작하는걸 추천드립니다.


Android 광고 ID 사용

Google Play 서비스 버전 4.0에서는 광고 및 분석 제공업체에서 사용할 수 있는 새 API 및 ID를 도입했습니다. 이러한 ID 사용에 대한 약관은 아래와 같습니다.

  • 사용: Android 광고 식별자는 광고 및 사용자 분석에만 사용해야 합니다. 각 ID 액세스에서 '관심기반 광고 선택 해제' 또는 '광고 맞춤설정 선택 해제' 설정 상태를 확인해야 합니다.
  • 개인 식별 정보 또는 기타 식별자와 연결: 광고 식별자는 사용자의 명시적인 동의 없이 개인 식별 정보에 연결되거나 영구적인 기기 식별자(예: SSAID, MAC 주소, IMEI, 등)에 연결되면 안 됩니다.
  • 사용자 선택 존중: 재설정 시 사용자의 명시적인 동의 없이 새 광고 식별자를 기존 광고 식별자 또는 기존 광고 식별자에서 파생된 데이터에 연결하면 안 됩니다. 또한 사용자의 '관심기반 광고 선택 해제' 또는 '광고 맞춤설정 선택 해제' 설정도 준수해야 합니다. 사용자가 이 설정을 사용하는 경우 광고 식별자를 사용하여 광고 목적으로 사용자 프로필을 만들거나 맞춤 광고로 사용자를 타겟팅하면 안 됩니다. 허용되는 작업에는 문맥 광고, 게재빈도 설정, 전환 추적, 보고 및 보안, 사기 감지 등이 있습니다.
  • 사용자에 대한 투명성: 광고 식별자의 수집과 사용 및 이러한 약관을 준수하기 위한 노력은 법적으로 적합한 개인정보 보호 고지를 통해 사용자에게 공개되어야 합니다. Google의 개인정보 보호 기준에 대해 자세히 알아보려면 사용자 데이터 정책을 검토하시기 바랍니다.
  • 이용약관 준수. 광고 식별자는 이러한 약관에 따라서만 사용할 수 있으며, 비즈니스 진행 과정에서 식별자를 공유할 수 있는 모든 관계자의 경우에도 마찬가지입니다. Google Play에 업로드되거나 게시되는 모든 앱은 광고 목적으로 다른 기기 식별자 대신 광고 ID(기기에서 사용 가능한 경우)를 사용해야 합니다.

광고 아이디는 유저가 임의대로 변경할수있기때문에 중요한 핵심 키값으로 사용하면 안되고 참고자료 정도로만 사용되어야 합니다. 광고 아이디를 변경할수있는 이유는 개인정보약관에 따른 법이 있다고 하는데 자세한건 구글 공홈을 참고해주세요. ㅎㅎ

 

일단 성공과 실패를 알려줄 리스너들을 생성해줍니다.

    public interface GAIDCallback{
        fun onSucces(result: String?)
        fun onFail(e:Exception)
    }

 

 

광고아이디를 가저오기위해선 백그라운드에서 진행해야하기 때문에  AsyncTask클래스를 하나 만들어 주세요.

    class GAIDTask(): AsyncTask<Void, Void, String>() {

        override fun doInBackground(vararg params: Void?): String? {
            
        }

        override fun onPreExecute() {
 
        }

        override fun onPostExecute(result: String?) {

        }
    }

 

 

 

필요한 변수들을 생성자로 받아와줍니다.

class GAIDTask(ctx: Context, callback: GAIDCallback):
    AsyncTask<Void, Void, String>() {
        private var ctx: Context = ctx
        private var idCallback: GAIDCallback = callback
        private lateinit var mException: Exception
}

 

 

 

그후 doInBackground 펑션에 광고아이디를 가저오는 로직을 추가하고  onPostExecute에서 결과값을 리스너에 알려줍니다.

    class GAIDTask(ctx: Context, callback: GAIDCallback): AsyncTask<Void, Void, String>() {
        private var ctx: Context = ctx
        private var idCallback: GAIDCallback = callback
        private lateinit var mException: Exception

        override fun onPreExecute() {
            super.onPreExecute()
            // ...
        }
        
        override fun doInBackground(vararg params: Void?): String? {
            var idInfo: AdvertisingIdClient.Info? = null

            try {
                idInfo = AdvertisingIdClient.getAdvertisingIdInfo(ctx)
            } catch (e: GooglePlayServicesNotAvailableException) {
                mException = e
            } catch (e: GooglePlayServicesRepairableException) {
                mException = e
            } catch (e: Exception) {
                mException = e
            }
            var advertId: String? = null
            try {
                advertId = idInfo!!.id
            } catch (e: Exception) {
                mException = e
            }
            return advertId
        }



        override fun onPostExecute(result: String?) {
            Log.i("tag","## result : "+ result);
            if (idCallback!=null && result!=null){
                idCallback.onSucces(result)
            } else{
                idCallback.onFail(mException)
            }
        }
    }

 

 

 

유연한 메모리 관리를 위해 가저온 context를 weakrefernce로 다시 구현

class GoogleUtils {

    public interface GAIDCallback{
        fun onSucces(result: String?)
        fun onFail(e:Exception)
    }

    class GAIDTask(ctx: Context, callback: GAIDCallback): AsyncTask<Void, Void, String>() {

        private var ctx: WeakReference<Context> =  WeakReference<Context>(ctx)
        private var idCallback: GAIDCallback = callback
        private lateinit var mException: Exception

        override fun onPreExecute() {
            super.onPreExecute()
            // ...
        }


        override fun doInBackground(vararg params: Void?): String? {
            var idInfo: AdvertisingIdClient.Info? = null

            try {
                idInfo = AdvertisingIdClient.getAdvertisingIdInfo(ctx.get())
            } catch (e: GooglePlayServicesNotAvailableException) {
                mException = e
            } catch (e: GooglePlayServicesRepairableException) {
                mException = e
            } catch (e: Exception) {
                mException = e
            }
            var advertId: String? = null
            try {
                advertId = idInfo!!.id
            } catch (e: Exception) {
                mException = e
            }
            return advertId
        }

        override fun onPostExecute(result: String?) {
            Log.i("tag","## result : "+ result);


            if (idCallback!=null && result!=null){
                idCallback.onSucces(result)
            } else{
                idCallback.onFail(mException)
            }

        }
    }


}

사용해보기

            GoogleUtils.GAIDTask(context, object : GoogleUtils.GAIDCallback {
                override fun onSucces(result: String?) {
                    if (result != null) {
                        toast("## result : " + result)
                    }
                }

                override fun onFail(e: Exception) {
                    if (e != null) {
                        toast("## ecepttion : " + e)
                    }
                }

            }).execute()

- 리스너로 간단하게 백그라운드로 동작해 메인 쓰레드에 결과값을 찍어줄수 있습니다.

 

끝.!

반응형
반응형

스토어로 이동

-안드로이드

https://play.google.com/store/apps/details?id=com.your.appname

 

-아이폰

https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=12341234yourid

itms-apps://itunes.apple.com/kr/app/apple-store/{app이름}

if (UIApplication.main.canOpen){
  if let appUrl = URL(string: "itms-apps://itunes.apple.com/kr/app/apple-store/{app이름}") { 
    if UIApplication.shared.canOpenURL(appUrl) { 
        UIApplication.shared.openURL(appUrl)
    } 
    else{
   		 //안열릴때 처리
    }
  }
}

 

앱스키마로 앱실행시 처리

*안드로이드

 

요청하는 부분

String uriData = schemeName+"://"+uriData;
Uri uri = Uri.parse(uriData);
Intent i = new Intent(Intent.ACTION_VIEW);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setData(uri);
startActivity(i);

- uriData는 이런식으로 되있다. - > myScheme://action?url='yoururl'

 

 

받는 부분

<application>
		....
      <activity
            android:name=".ui.MainActivity"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme.Base"
            android:windowSoftInputMode="adjustPan">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:host="action"
                    android:scheme="myScheme" />
            </intent-filter>
        </activity>
        ....
</application>

 

<data android:host="action"
	  android:scheme="myScheme" />

- host : 빨간색

  scheme : 파란색

 

  myScheme://action?url='yoururl'

 

 

스키마처리 ..

   public void checkExternalAppschema2(){
        Logger.d(TAG,"## checkExternalAppschema!!");
        //다른앱에서 실행시킬때 받을 인텐트
        Intent schemeIntent = getIntent();
        if (schemeIntent.getAction() != null){
            if (Intent.ACTION_VIEW.equals(schemeIntent.getAction())) {
                Uri uri = null;
                uri = schemeIntent.getData();
                if(uri != null) {
                    app_schema_url = uri.getQueryParameter("url");

                    // 파라미터가 있을때.
                    if (app_schema_url != null){
                        new Handler().postDelayed(new Runnable() {
                            @Override
                            public void run() {
                              //  파라미터가 있을때 처리 ...
                            }
                        }, 500);

                        try{
                            getIntent().replaceExtras(new Bundle());
                            getIntent().setAction("");
                            getIntent().setData(null);
                            getIntent().setFlags(0);
                        }catch (NullPointerException e){
                            e.printStackTrace();
                        }
                    }
                    //파라미터가 없을시
                    else {
                       //  파라미터가 없을시 처리 ...
                    }
                }
            }
        } 
    }

 

app_schema_url = uri.getQueryParameter("url");

- 쿼리파라미터 키,벨류 ; 초록색

   myScheme://action?url='yoururl'

 

 

getIntent().replaceExtras(new Bundle());
getIntent().setAction("");
getIntent().setData(null);
getIntent().setFlags(0);

- 초기화 안시켜주면 한번더 요청이 왔을때 작동을 안한다.

 

 

 

 

 

*아이폰

보내는쪽

            if let appurl = URL(string: "yourapp://") {
                if  UIApplication.shared.canOpenURL(appurl){
                    UIApplication.shared.open(appurl, options: .init()) { (finished) in
                        if finished {
                            print("finished !!")
                        }
                    }
                }
            }

받는쪽

  func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    if let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) {
      if let queryItems = urlComponents.queryItems {
        if let top = topView() {
          nToast.show(top, msg: "queryItems: \(queryItems.description)")
          
//          let key = queryItems.first?.name
//          let val = queryItems.first?.value
//          queryItems.forEach { item in }
        }
      }
    }
    
    //다른앱에서 오픈했을때.
    if let otherApp = options[.sourceApplication] as? String {
        logi("\(TAG) open url sending App ID = \(otherApp)")
    }
    //safari에서 오픈했을때.
    else {
         logi("\(TAG) open url safari ")
    }
    
    
    
    return true
  }
반응형
반응형

웹뷰를 띄우기전 기본구성 - objectivC

//make configuration, share session pool
-(WKWebViewConfiguration*)createWebviewConfiguration:(UIViewController*)vc shareData:(CustomShare*)shareData{
    //webview function setting
    WKPreferences *pref = [[WKPreferences alloc] init];
    pref.javaScriptEnabled = true;
    pref.javaScriptCanOpenWindowsAutomatically = true;
    
    // web에서 호출할 펑션이름 webkit.messageHandlers.AppFunction.postMessage("원하는 데이터")
    WKUserContentController *contentController = [[WKUserContentController alloc] init];
    [contentController addScriptMessageHandler:vc name:@"AppFunction"];
    
    //html이 완전히 로드됬을때 스크립트 주입하도록 WKUserScriptInjectionTimeAtDocumentEnd
    WKUserScript *userScript = [[WKUserScript alloc] initWithSource:@"" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:true];
    [contentController addUserScript:userScript];
    
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    config.userContentController = contentController;
    config.preferences = pref;
    config.processPool = [shareData sharedWebViewPool];//세션풀을 공유하기위한 부분
    return config;
}

세션풀 공유를위해 CustomShare

- (WKProcessPool *)sharedWebViewPool {
    static WKProcessPool *pool;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (!pool) {
            pool = [[WKProcessPool alloc] init];
        }
    });
    return pool;
}

웹뷰를 띄우기전 기본구성 - swift

import UIKit
import WebKit

class CMWkWebview: NSObject {
    public static var HandlerName = "AppFunction"
    
    //not use session pool
    public static func createWebviewConfiguration(handler:WKScriptMessageHandler) -> WKWebViewConfiguration{
        let pref = WKPreferences()
        pref.javaScriptEnabled = true
        pref.javaScriptCanOpenWindowsAutomatically = true;
        
        let script = WKUserScript(source: "", injectionTime: .atDocumentEnd, forMainFrameOnly: true)
        let contentCon = WKUserContentController()
        
        contentCon.addUserScript(script)
        contentCon.add(handler, name: HandlerName)
        
        let config = WKWebViewConfiguration()
        config.userContentController = contentCon
        config.preferences = pref
        
        return config
    }
    
    //use session pool
    public static func createWebviewConfiguration(handler:WKScriptMessageHandler, processPool:WKProcessPool) -> WKWebViewConfiguration{
        let pref = WKPreferences()
        pref.javaScriptEnabled = true
        pref.javaScriptCanOpenWindowsAutomatically = true;
        
        let script = WKUserScript(source: "", injectionTime: .atDocumentEnd, forMainFrameOnly: true)
        let contentCon = WKUserContentController()
        
        contentCon.addUserScript(script)
        contentCon.add(handler, name: HandlerName)
        
        let config = WKWebViewConfiguration()
        config.userContentController = contentCon
        config.preferences = pref
        config.processPool = processPool
        
        return config
    }
}

wkprocesspool을 사용함으로써 새로 웹뷰를 생성해도 processpool만 메모리에 보존된다면 session을 유지한상태로 웹뷰를 구성할수있다.

 

 


사용 - objectivC

@interface HomeViewController : BaseViewController<WKScriptMessageHandler>
@end

- WKScriptMessageHandler 프로토콜을 가저와서

 

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    
    NSString *msg = message.body;
    
    if ( [[message name] isEqualToString:@"AppFunction"] ){
        
        //custom function
    }
    
}

-이곳에 커스텀 펑션을 구현하면된다.

 

사용 - swift

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let configuration = CMWkWebview.createWebviewConfiguration(handler: self)
        let webview = WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
        self.view.addSubview(webview)
       
    }
extension YourController :WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == CMWkWebview.HandlerName {
            print("web data : \(message.body)")
        }
    }
}

web에서 작동

webkit.messageHandlers.AppFunction.postMessage("원하는 데이터")

- 웹쪽에선 버튼이나 onLoad function 등 ... 원하는곳이 파라미터를 넣어서 앱과 통신할수있다.

반응형

+ Recent posts