728x90

출처 : stackoverflow.com/questions/58712648/getting-a-permission-error-even-after-permission-read-contacts-is-declared

public class MainActivity extends AppCompatActivity {

    // Request code for READ_CONTACTS. It can be any number > 0.
    private static final int PERMISSIONS_REQUEST_READ_CONTACTS = 100;

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

        // Read and show the contacts
        showContacts();
    }

    /**
     * Show the contacts in the ListView.
     */
    private void showContacts() {
        // Check the SDK version and whether the permission is already granted or not.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, PERMISSIONS_REQUEST_READ_CONTACTS);
            //After this point you wait for callback in onRequestPermissionsResult(int, String[], int[]) overriden method
        } else {
            // Android version is lesser than 6.0 or the permission is already granted.
            getContactNames();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions,
                                           int[] grantResults) {
        if (requestCode == PERMISSIONS_REQUEST_READ_CONTACTS) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission is granted
                showContacts();
            } else {
                Toast.makeText(this, "Until you grant the permission, we canot display the names", Toast.LENGTH_SHORT).show();
            }
        }
    }

    /**
     * Read the name of all the contacts.
     *
     * @return a list of names.
     */
    private void getContactNames() {
        Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
        String[] projection    = new String[]          {ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
                ContactsContract.CommonDataKinds.Phone.NUMBER};

        Cursor people = getContentResolver().query(uri, projection, null,  null, null);

        int indexName = people.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
        int indexNumber = people.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);

        if(people.moveToFirst()) {
           do {
              String name   = people.getString(indexName);
              String number = people.getString(indexNumber);
              // add number to list
        // Do work...
           } while (people.moveToNext());
         }
         people.close();
    }
}
블로그 이미지

Link2Me

,
728x90

Handler 코드를 아래와 같이 수정해야 제대로 동작된다.

Java  Handler handler = new Handler(Looper.getMainLooper());
kotlin  val handler = Handler(Looper.getMainLooper())

 

다른 앱 호출 및 실행

https://developer.android.com/training/basics/intents/package-visibility?hl=ko 참조(패키지 공개상태 관리)

안드로이드 11 에서 QUERY_ALL_PACKAGES 권한이 도입되었다.

간단하게 아래 한줄을 추가해주면 어플에서 내비 등을 호출할 때 이상없이 동작된다.

<!-- 다른 앱 실행 : SDKVersion 30 이상은 권한 추가 필요 -->
<permission android:name="android.permission.QUERY_ALL_PACKAGES" />

 

지정한 특정 앱만 실행하도록 하는 것은

<queries>
    <package android:name="kt.navi" />
    <package android:name="com.locnall.KimGiSa" />
    <package android:name="com.nhn.android.nmap" />
</queries>

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.link2me.android.map">
 
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.READ_SMS" />
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    <!-- 다른 앱 실행 : SDKVersion 30 이상은 권한 추가 필요 -->
    <permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
    <queries>
        <intent>
            <action android:name="android.intent.action.MAIN" />
        </intent>
    </queries>
 
    <queries>
        <package android:name="kt.navi" />
        <package android:name="com.locnall.KimGiSa" />
        <package android:name="com.nhn.android.nmap" />
    </queries>
 
    <application
        android:name=".GlobalApplication"
        android:allowBackup="true"
        android:extractNativeLibs="true"
        android:icon="@drawable/map_icon"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:usesCleartextTraffic="true">
 
 
        <meta-data
            android:name="com.naver.maps.map.CLIENT_ID"
            android:value="@string/naver_app_key" />
        <meta-data
            android:name="com.kakao.sdk.AppKey"
            android:value="@string/kakao_app_key" />
    </application>
 
</manifest>

 

참고하면 좋은 글 : https://codechacha.com/ko/android11-package-visibility/

 

블로그 이미지

Link2Me

,
728x90

LinearLayout 에서 horizontal 로 검색어를 배치하는 방법이다.

<LinearLayout 
     android:layout_height="wrap_content" 
     android:layout_width="match_parent"
     android:orientation="horizontal" >
<EditText
     android:layout_height="wrap_content" 
     android:layout_weight="1"
     android:layout_width="0dp"/>
 <Button
    android:text="Button"
     android:layout_width="wrap_content" 
     android:layout_height="wrap_content"/>
 </LinearLayout>

별로 깔끔한 디자인이 아니라서 Material Design 을 적용해서 테스트해 봤다.

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <com.google.android.material.textfield.TextInputLayout
	style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
	android:layout_width="0dp"
	android:layout_height="wrap_content"
	android:layout_weight="1"
	app:shapeAppearance="@style/ShapeAppearance.MaterialComponents.MediumComponent"
	app:boxStrokeColor="@color/blue_500"
	app:endIconMode="clear_text"
	android:hint="검색어"
	android:layout_marginLeft="5dp"
	android:layout_marginRight="5dp">

	<com.google.android.material.textfield.TextInputEditText
	    android:id="@+id/list_search_edit"
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content" />

    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.button.MaterialButton
	android:id="@+id/list_search_btn"
	style="@style/Widget.MaterialComponents.Button.OutlinedButton"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:layout_gravity="center_vertical"
	android:layout_marginRight="5dp"
	app:strokeColor="@color/blue_700"
	app:strokeWidth="1dp"
	android:text="검색"
	android:textSize="18sp" />

</LinearLayout>

그러나 원하는 결과가 나오지 않았다.

 

android SearchView 기능을 이용하여 원하는 결과를 얻을 수 있다.

<androidx.appcompat.widget.SearchView
    android:id="@+id/list_search_edit"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:clickable="true"
    app:iconifiedByDefault="false"
    app:queryHint="검색어를 입력하세요"/>

Java 소스코드 처리

import androidx.appcompat.widget.SearchView;

SearchView search_view;
private ArrayList<Group_Item> groupItemList = new ArrayList<>(); // 서버에서 가져온 원본 데이터 리스트

search_view = findViewById(R.id.list_search_edit);
search_view.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    @Override
    public boolean onQueryTextSubmit(String query) {
	// do something on text submit
	searchData(query);
	return false;
    }

    @Override
    public boolean onQueryTextChange(String newText) {
	if(newText.length() == 0){
	    groupItemList.clear();
	}
	return false;
    }
});

검색어를 입력할 때마다 데이터를 서버에서 가져오는 것은 원하는 결과가 아닌 거 같아서

검색어를 입력하고 나서 검색 아이콘을 눌렀을 때 서버로부터 결과를 가져오고,

검색어를 삭제하면 ArrayList 데이터를 초기화하는 로직을 사용했다.

블로그 이미지

Link2Me

,
728x90
수정 편집을 하려고 하면 에디터가 엉망으로 글을 써주는 통에 적응이 안된다.

fun getMyIPaddress(){
    val gson = GsonBuilder().setLenient().create()
    val retrofit = Retrofit.Builder()
        .baseUrl("http://checkip.amazonaws.com/")
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build()
    val service = retrofit.create(RetrofitService.IRetrofit::class.java)

    service.getMyIPAddress().enqueue(object: Callback<String> {
        override fun onResponse(call: Call<String>, response: Response<String>) {
            response.body()?.let {
                PrefsHelper.write("myIPaddress",it) // 평문 저장
                Log.e(TAG,"myIPaddress : ${it}")
            }
        }
        override fun onFailure(call: Call<String>, t: Throwable) {

        }
    })
}



블로그 이미지

Link2Me

,
728x90

앱의 IP 주소를 알아내는 코드이다.

 

구글 검색하면 가장 많이 나오는 코드

fun getIPAddress(useIPv4: Boolean): String {
    try {
    val interfaces: List<NetworkInterface> = Collections.list(NetworkInterface.getNetworkInterfaces())
    for (intf in interfaces) {
        val addrs: List<InetAddress> = Collections.list(intf.inetAddresses)
        for (addr in addrs) {
        if (!addr.isLoopbackAddress) {
            val sAddr = addr.hostAddress.toUpperCase()
            val isIPv4 = InetAddressUtils.isIPv4Address(sAddr)
            if (useIPv4) {
            if (isIPv4) return sAddr
            } else {
            if (!isIPv4) {
                val delim = sAddr.indexOf('%') // drop ip6 port suffix
                return if (delim < 0) sAddr else sAddr.substring(0, delim)
            }
            }
        }
        }
    }
    } catch (e: Exception) {
    e.printStackTrace()
    }
    return ""
}
 

My IP Address 앱을 다운로드 받아서 실행해보면, Local IP 로 결과를 반환한다.

 

블로그 이미지

Link2Me

,
728x90

RSA 암호화 예제를 찾으니 https://travistran.me/rsa-encryption-in-java-and-javascript-1275/ 가 검색되었다.

이 코드를 코틀린으로 변환하여 테스트 해본 것이다.

 

앱 build.gradle 추가사항

// RSA 암호화

implementation 'org.bouncycastle:bcpkix-jdk15on:1.56'

implementation 'javax.xml.bind:jaxb-api:2.2.4'

 

import com.link2me.android.common.TravisRsa.DataTypeEnum
import com.link2me.android.common.TravisRsa.ModeEnum
import kotlin.Throws
import kotlin.jvm.JvmOverloads
import com.link2me.android.common.TravisRsa
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.io.Serializable
import java.lang.Exception
import java.nio.charset.StandardCharsets
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher
import javax.xml.bind.DatatypeConverter

class TravisRsa : Serializable {
    enum class ModeEnum {
        PKCS1, OAEP
    }

    enum class DataTypeEnum {
        HEX, BASE64
    }

    var dataType = DataTypeEnum.BASE64
    var mode = ModeEnum.PKCS1
    var privateKey: PrivateKey? = null
    var publicKey: PublicKey? = null

    constructor() {
        try {
            val keyGen = KeyPairGenerator.getInstance("RSA")
            keyGen.initialize(2048)
            val pair = keyGen.generateKeyPair()
            privateKey = pair.private
            publicKey = pair.public
        } catch (e: Exception) {
           
        }
    }

    constructor(keySize: Int) {
        try {
            val keyGen = KeyPairGenerator.getInstance("RSA")
            keyGen.initialize(keySize)
            val pair = keyGen.generateKeyPair()
            privateKey = pair.private
            publicKey = pair.public
        } catch (e: Exception) {
          
        }
    }

    @Throws(Exception::class)
    fun encrypt(plainText: String, publicKey: PublicKey?): ByteArray {
        val cipher = cipher
        cipher.init(Cipher.ENCRYPT_MODE, publicKey)
        return cipher.doFinal(plainText.toByteArray(StandardCharsets.UTF_8))
    }

    @Throws(Exception::class)
    fun decrypt(cipherText: ByteArray?, privateKey: PrivateKey?): ByteArray {
        val cipher = cipher
        cipher.init(Cipher.DECRYPT_MODE, privateKey)
        return cipher.doFinal(cipherText)
    }

    @JvmOverloads
    @Throws(Exception::class)
    fun encrypt(
        plainText: String,
        base64PublicKey: String? = getBase64PublicKey(publicKey!!)
    ): String {
        val cipherText = encrypt(plainText, getPublicKey(base64PublicKey))
        return if (DataTypeEnum.BASE64 == dataType) {
            toBase64(cipherText)
        } else {
            toHex(cipherText)
        }
    }

    @JvmOverloads
    @Throws(Exception::class)
    fun decrypt(
        cipherText: String?,
        base64PrivateKey: String? = getBase64PrivateKey(privateKey!!)
    ): String {
        val cipherBytes: ByteArray
        cipherBytes = if (DataTypeEnum.BASE64 == dataType) {
            fromBase64(cipherText)
        } else {
            fromHex(cipherText)
        }
        return String(decrypt(cipherBytes, getPrivateKey(base64PrivateKey)), StandardCharsets.UTF_8)
    }

    @get:Throws(Exception::class)
    private val cipher: Cipher
        private get() = if (ModeEnum.OAEP == mode) {
            Cipher.getInstance(
                "RSA/ECB/OAEPWithSHA1AndMGF1Padding",
                BouncyCastleProvider()
            )
        } else {
            Cipher.getInstance("RSA/ECB/PKCS1Padding")
        }

    companion object {

        fun getBase64PublicKey(publicKey: PublicKey): String {
            return toBase64(publicKey.encoded)
        }

        fun getBase64PrivateKey(privateKey: PrivateKey): String {
            return toBase64(privateKey.encoded)
        }

        fun getPublicKey(base64PublicKey: String?): PublicKey? {
            try {
                val keySpec = X509EncodedKeySpec(fromBase64(base64PublicKey))
                return KeyFactory.getInstance("RSA").generatePublic(keySpec)
            } catch (e: Exception) {
               
            }
            return null
        }

        fun getPrivateKey(base64PrivateKey: String?): PrivateKey? {
            try {
                val keySpec = PKCS8EncodedKeySpec(fromBase64(base64PrivateKey))
                return KeyFactory.getInstance("RSA").generatePrivate(keySpec)
            } catch (e: Exception) {
               
            }
            return null
        }

        private fun fromBase64(str: String?): ByteArray {
            return DatatypeConverter.parseBase64Binary(str)
        }

        private fun toBase64(ba: ByteArray): String {
            return DatatypeConverter.printBase64Binary(ba)
        }

        private fun fromHex(str: String?): ByteArray {
            return DatatypeConverter.parseHexBinary(str)
        }

        private fun toHex(ba: ByteArray): String {
            return DatatypeConverter.printHexBinary(ba)
        }
    }
}

fun main(args: Array<String>) {
    val input = "안녕하세요. 반갑습니다."
    println(input.length)
    println("byte array length=" + input.toByteArray().size)

    val travisRsa = TravisRsa()
    val encrypted = travisRsa.encrypt(input)
    val decrypted = travisRsa.decrypt(encrypted)

    println("encrypted : $")
    println("decrypted : $")
}
 

 

 

블로그 이미지

Link2Me

,
728x90

코틀린에서 RecyclerViewAdapter를 만드는 방법을 이미지 순서로 만들었다.


class ContactsListAdapter(val itemList: List<Contacts>) : RecyclerView.Adapter<ContactsListAdapter.ContactsViewHolder>() {


}








이렇게 하고 나서 아래코드까지 코드를 구현한다.


class ContactsListAdapter(val itemList: List<Contacts>) : RecyclerView.Adapter<ContactsListAdapter.ContactsViewHolder>() {

    class ContactsViewHolder(v: View) : RecyclerView.ViewHolder(v) {

    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ContactsListAdapter.ContactsViewHolder {
        TODO("Not yet implemented")
    }

    override fun onBindViewHolder(holder: ContactsListAdapter.ContactsViewHolder, position: Int) {
        TODO("Not yet implemented")
    }

    override fun getItemCount(): Int {
        TODO("Not yet implemented")
    }
}
 


import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.telephony.PhoneNumberUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.link2me.android.sqlite.R
import com.link2me.android.sqlite.model.ContactData
import com.link2me.android.sqlite.utils.Value
import kotlinx.android.synthetic.main.address_item.view.*

class ContactsListAdapter(val context: Context, val itemList: List<ContactData>) : RecyclerView.Adapter<ContactsListAdapter.ContactsViewHolder>() {
    private val TAG = this.javaClass.simpleName

    // itemClockListener 를 위한 인터페이스 정의
    interface OnItemClickListener {
        fun onClick(v: View, position: Int)
    }
    private lateinit var itemClickListener: OnItemClickListener

    fun  setItemClickListener(itemClickListener: OnItemClickListener){
        this.itemClickListener = itemClickListener
    }
    //////////////////////////////////////////////////////////////////

    // RecyclerView 초기화때 호출된다.
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactsViewHolder {
        // ViewHolder에 쓰일 Layout을 inflate하는 함수
        val rootView = LayoutInflater.from(parent.context).inflate(R.layout.address_item, parent,false)
        return ContactsViewHolder(rootView,context)
        // 뷰 홀더가 생성된 후 RecyclerView가 뷰 홀더를 뷰의 데이터에 바인딩한다.
    }

    // 생성된 View에 보여줄 데이터를 설정
    override fun onBindViewHolder(holder: ContactsViewHolder, position: Int) {
        // RecyclerView는 ViewHolder를 데이터와 연결할 때 이 메서드를 호출한다.
        val item = itemList[position]

        holder.itemView.setOnClickListener {
            itemClickListener.onClick(it,position)
        }

        holder.apply {
            bind(item)
        }
    }

    override fun getItemCount(): Int = itemList.size

    class ContactsViewHolder(var view: View, val context: Context) : RecyclerView.ViewHolder(view) {
        // View를 저장할 수 있는 변수
        val img_photo = view.findViewById<ImageView>(R.id.profile_Image)
        val tv_name = view.findViewById<TextView>(R.id.child_name)
        val tv_mobileNO = view.findViewById<TextView>(R.id.child_mobileNO)
        val btn_phonecall = view.findViewById<ImageView>(R.id.child_Btn)

        // View와 데이터를 연결시키는 함수
        fun bind(item: ContactData){
            if (item.photo.contains("jpg")){
                val photoURL = Value.Photo_URL + item.photo
                Glide.with(context).load(photoURL).into(img_photo)
            }

            tv_name.text = item.userNM
            tv_mobileNO.text = item.mobileNO

            val items = arrayOf("휴대폰 전화걸기", "연락처 저장")
            val builder = AlertDialog.Builder(context)
            builder.setTitle("해당작업을 선택하세요")
            builder.setItems(items, DialogInterface.OnClickListener { dialog, which ->
                Toast.makeText(context, items[which] + "선택했습니다.", Toast.LENGTH_SHORT).show()
                when (which) {
                    0 -> {
                        if (item.mobileNO.length == 0) {
                            Toast.makeText(context, "전화걸 휴대폰 번호가 없습니다.", Toast.LENGTH_SHORT).show()
                            return@OnClickListener
                        }
                        val dialog1 = AlertDialog.Builder(context)
                            .setTitle(item.userNM)
                            .setMessage(PhoneNumberUtils.formatNumber(item.mobileNO) + " 통화하시겠습니까?")
                            .setPositiveButton("예",
                                { dialog, which ->
                                    val intent = Intent(Intent.ACTION_CALL,
                                        Uri.parse("tel:" + PhoneNumberUtils.formatNumber(item.mobileNO))
                                    )
                                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                                    context.startActivity(intent)
                                })
                            .setNegativeButton("아니오") { dialog, which -> dialog.dismiss() }.create()
                        dialog1.show()
                    }
                    1 -> Toast.makeText(context, "전화번호 저장하는 로직은 직접 구현하시기 바랍니다.", Toast.LENGTH_SHORT).show()
                }
            })
            builder.create()
            btn_phonecall.setOnClickListener { builder.show() }
        }
    }
}
 


사용 예제에 대한 전체 코드는 GitHub 에 올려뒀다.

https://github.com/jsk005/KotlinProjects/tree/master/retrofit


블로그 이미지

Link2Me

,
728x90

코틀린에서 Retrofit2 라이브러리를 이용하여 서버 데이터를 가져오는 기능을 처리하는데 헷갈려서 정리를 좀 해둔다.


예제1)


먼저 PHP 코드에서 JSON 데이터를 만드는 방법

    $R = array(); // 결과 담을 변수 생성
    $result = $c->putDbArray($sql);
    while($row = $result->fetch_assoc()) {
        if($row['photo'] == NULL) {
            $row['photo'] = "";
        } else {
            $path = "./photos/".$row['photo'];
            if(!file_exists($path)) {
                $row['photo'] = "";
            }
        }
        array_push($R, $row);
    }
    header("Cache-Control: no-cache, must-revalidate");
    header("Content-type: application/json; charset=UTF-8");

    $status = "success";
    $result = array(
        'status' => $status,
        'message' => $R
    );
    echo json_encode($result);
 

while 문을 사용했다는 것은 $R 변수는 배열이라는 것을 명심하자.


서버에서 받은 데이터를 Retrofit 라이브러리에서 받아서 처리하기 위한 data class

@Parcelize
data class ContactData (
    // PersonData 정보를 담고 있는 객체 생성
    var idx: String,
    var userNM: String = "",
    var mobileNO: String = "",
    var officeNO: String = "",
    var photo: String = "", // 이미지 경로를 String으로 받기 위해서
    var isCheckBoxState: Boolean = false
): Parcelable
 

@Parcelize
class ContactDataResult (
    val status: String,
    val message: List<ContactData>
): Parcelable

interface IRetrofit {

    @FormUrlEncoded
    @POST(Value.Contacts)
    fun getContactDataResult(@Field("idx") idx: String): Call<ContactDataResult>
}
 


Retrofit 라이브러리 사용법

object RetroClient { // 싱글턴
    // 레트로핏 클라어언트 선언
    private var retrofitClient: Retrofit? = null
    var instance: IRetrofit? = null

    val gson = GsonBuilder().setLenient().create()

    // 레트로핏 클라어언트 가져오기
    fun getClient(baseUrl: String): Retrofit? {
        if (retrofitClient == null){
            retrofitClient = Retrofit.Builder()
                .baseUrl(baseUrl)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .client(Utils.createOkHttpClient())
                .build()
        }
        return retrofitClient
    }

    @JvmName("getInstance1")
    fun getInstance(): IRetrofit? {
        if (instance == null) {
            instance = getClient(Value.API_BASE_URL)?.create(IRetrofit::class.java)
        }
        return instance
    }

}
 

class Value : AppCompatActivity() {
    companion object{
        const val API_BASE_URL = "https://test.abc.com/androidSample/"
        const val Photo_URL = "https://test.abc.com/androidSample/photos/"

        const val Contacts = "getContactData.php"
    }
}
 

   private fun getServerData() {
        RetroClient.getInstance()?.getContactDataResult("1")?.enqueue(object :
            Callback<ContactDataResult> {
            override fun onResponse(call: Call<ContactDataResult>, response: Response<ContactDataResult>) {
                if (response.body()!!.status.contains("success")){
                    addressItemList.clear()
                    searchItemList.clear()

                    val contactData = response.body()!!.message
                    for (item in contactData){
                        addressItemList.add(item)
                        searchItemList.add(item)
                    }

                    runOnUiThread { // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged()
                    }
                }
            }

            override fun onFailure(call: Call<ContactDataResult>, t: Throwable) {
                Log.e(TAG,"Retrofit response fail.")
            }

        })

    }
 



예제2)

PHP 코드 수정사항

echo json_encode(array('message'=>$R));


결과 형태

{"message":[{"idx":1,"userNM":"\uac1c\ubc1c\uc790","mobileNO":"01000010001","telNO":"0234560001","photo":"1.jpg"},
{"idx":2,"userNM":"\uc774\uc815\uc740","mobileNO":"01001230001","telNO":"","photo":"2.jpg"},
{"idx":3,"userNM":"\uae40\ud64d\uae38","mobileNO":"01001230002","telNO":"","photo":""},
{"idx":4,"userNM":"\ucd5c\uc2e0\ud615","mobileNO":"01001230003","telNO":"","photo":"4.jpg"}]}
 


코틀린 수정사항

@Parcelize
class ContactDataResult (
    val message: List<ContactData>
): Parcelable

// 수정사항 없음
@Parcelize
data class ContactData (
    // PersonData 정보를 담고 있는 객체 생성
    var idx: String,
    var userNM: String = "",
    var mobileNO: String = "",
    var officeNO: String = "",
    var photo: String = "", // 이미지 경로를 String으로 받기 위해서
    var isCheckBoxState: Boolean = false
): Parcelable
 


        RetroClient.getInstance()?.getContactDataResult("1")?.enqueue(object :
            Callback<ContactDataResult> {
            override fun onResponse(call: Call<ContactDataResult>, response: Response<ContactDataResult>) {
                if (response.isSuccessful){
                    addressItemList.clear()
                    searchItemList.clear()

                    val contactData = response.body()!!.message
                    for (item in contactData){
                        addressItemList.add(item)
                        searchItemList.add(item)
                    }

                    runOnUiThread { // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged()
                    }
                }
            }

            override fun onFailure(call: Call<ContactDataResult>, t: Throwable) {
                Log.e(TAG,"Retrofit response fail.")
            }

        })
 


예제3)

PHP 코드 수정사항

echo json_encode($R);


JSON 결과==> 배열

[{"idx":1,"userNM":"\uac1c\ubc1c\uc790","mobileNO":"01000010001","telNO":"0234560001","photo":"1.jpg"},
 {"idx":2,"userNM":"\uc774\uc815\uc740","mobileNO":"01001230001","telNO":"","photo":"2.jpg"},
 {"idx":3,"userNM":"\uae40\ud64d\uae38","mobileNO":"01001230002","telNO":"","photo":""},
 {"idx":4,"userNM":"\ucd5c\uc2e0\ud615","mobileNO":"01001230003","telNO":"","photo":"4.jpg"},
 {"idx":5,"userNM":"\ud64d\uae38\ub3d9","mobileNO":"01000009880","telNO":"","photo":"5.jpg"}]
 


코틀린 수정사항


interface IRetrofit {
    @FormUrlEncoded
    @POST(Value.Contacts)
    fun getContactsList(@Field("idx") idx: String): Call<List<ContactData>>
}
 



        RetroClient.getInstance()?.getContactsList("1")?.enqueue(object :
            Callback<List<ContactData>> {
            override fun onResponse(call: Call<List<ContactData>>, response: Response<List<ContactData>>) {
                if (response.isSuccessful){
                    addressItemList.clear()
                    searchItemList.clear()

                    val contactData = response.body()!!
                    for (item in contactData){
                        addressItemList.add(item)
                        searchItemList.add(item)
                    }

                    runOnUiThread { // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged()
                    }
                }
            }

            override fun onFailure(call: Call<List<ContactData>>, t: Throwable) {
                Log.e(TAG,"Retrofit response fail.")
            }

        })
 



예제4) JSON 파싱하기
PHP 코드 수정사항
echo json_encode(array('result'=>$R));

JSON 결과

{"result":[{"idx":1,"userNM":"\uac1c\ubc1c\uc790","mobileNO":"01000010001","telNO":"0234560001","photo":"1.jpg"},
           {"idx":2,"userNM":"\uc774\uc815\uc740","mobileNO":"01001230001","telNO":"","photo":"2.jpg"},
       {"idx":3,"userNM":"\uae40\ud64d\uae38","mobileNO":"01001230002","telNO":"","photo":""},
       {"idx":4,"userNM":"\ucd5c\uc2e0\ud615","mobileNO":"01001230003","telNO":"","photo":"4.jpg"},
       {"idx":5,"userNM":"\ud64d\uae38\ub3d9","mobileNO":"01000009880","telNO":"","photo":"5.jpg"}]}
 


코틀린 수정사항

interface IRetrofit {
    @FormUrlEncoded
    @POST(Value.Contacts)
    fun getContactsObject(@Field("idx") idx: String): Call<JsonObject>
} 

object ContactContract {
    const val _RESULTS = "result" // 서버 정보를 파싱하기 위한 변수 선언

    object Entry : BaseColumns {
        const val TABLE_NAME = "PBbook"
        const val _IDX = "idx" // 서버 테이블의 실제 필드명
        const val _NAME = "userNM"
        const val _MobileNO = "mobileNO"
        const val _telNO = "telNO"
        const val _Team = "Team"
        const val _Mission = "Mission"
        const val _Position = "Position"
        const val _Photo = "photo" // 이미지 필드
        const val _Status = "status"
    } // SQLite의 데이터 타입은 NULL, INTEGER, REAL, TEXT, BLOB 만 지원한다.
}
 

=== MainActivity.kt 파일에서 발췌 ===

    private fun getServerData() {
        RetroClient.getInstance()?.getContactsObject("1")?.enqueue(object :
            Callback<JsonObject> {
            override fun onResponse(call: Call<JsonObject>, response: Response<JsonObject>) {
                if (response.isSuccessful){
                    addressItemList.clear()
                    searchItemList.clear()

                    val jsonObj = JSONObject(response.body().toString())
                    val person = jsonObj.getJSONArray(ContactContract._RESULTS)

                    for (i in 0 until person.length()) {
                        val c = person.getJSONObject(i)
                        val idx = c.getString(ContactContract.Entry._IDX)
                        val name = c.getString(ContactContract.Entry._NAME)
                        val mobileNO = c.getString(ContactContract.Entry._MobileNO)
                        val officeNO = c.getString(ContactContract.Entry._telNO)
                        val PhotoImage = c.getString(ContactContract.Entry._Photo)

                        Log.e(TAG,"name : " + name)

                        getAllDataList(idx, name, mobileNO, officeNO, PhotoImage, false)
                        selectDataList(idx, name, mobileNO, officeNO, PhotoImage, false)
                    }

                    runOnUiThread { // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged()
                    }
                }
            }

            override fun onFailure(call: Call<JsonObject>, t: Throwable) {
                Log.e(TAG,"Retrofit response fail.")
            }

        })

    }
 
    // 아이템 전체 데이터 추가 메소드
    fun getAllDataList(uid: String, name: String, mobileNO: String, officeNO: String, photo_image: String, checkItem_flag: Boolean) {
        val item = ContactData(uid, name, mobileNO, officeNO, photo_image, checkItem_flag)
        addressItemList.add(item)
    }

    // 선택한 데이터 추가 메소드
    fun selectDataList(uid: String, name: String, mobileNO: String, officeNO: String, photo_image: String, checkItem_flag: Boolean) {
        val item = ContactData(uid, name, mobileNO, officeNO, photo_image, checkItem_flag)
        searchItemList.add(item)
    }
 




블로그 이미지

Link2Me

,
728x90

Kotlin으로 SQLite를 활용해서 db생성, insert문, delete문, update문, select문 사용 방법에 대한 예제다.
서버에 있는 이미지는 URL 경로를 Glide 라이브러리를 이용하여 보여주는 방법으로 구현했다.


프로젝트 build.gradle

buildscript {
    ext.kotlin_version = "1.4.21"
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        def nav_version = "2.3.2"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://jitpack.io" }

    }
}

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



앱 build.gradle

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions'  // Android Studio 4.1부터 사용하지 말라고 권고하고 ViewBindg 사용 권장
    id 'kotlin-kapt'
}

// ViewBinding 은 Android Studio 3.6부터 사용 가능

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"

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

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'androidx.cardview:cardview:1.0.0'

    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'

    implementation 'com.github.bumptech.glide:glide:4.11.0'
    kapt 'com.github.bumptech.glide:compiler:4.11.0'

    // debugging 을 위한 툴
    implementation 'com.facebook.stetho:stetho:1.5.1'
    implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
    implementation 'com.facebook.stetho:stetho-js-rhino:1.5.1' // 콘솔을 이용한 명령어 입력기능

}
 


ContactContract.kt

import android.provider.BaseColumns

object ContactContract {
    const val _RESULTS = "result" // 서버 정보를 파싱하기 위한 변수 선언
    const val _CallingNO = "callingNO" // 전화 수신
    const val _CallingTime = "received_time"

    object Entry : BaseColumns {
        // BaseColumns 인터페이스를 구현함으로써 내부 클래스는 _ID라고 하는 기본 키 필드를 상속할 수 있다.
        const val TABLE_NAME = "PBbook"
        const val _IDX = "idx" // 서버 테이블의 실제 필드명
        const val _NAME = "userNM"
        const val _MobileNO = "mobileNO"
        const val _telNO = "telNO"
        const val _Team = "Team"
        const val _Mission = "Mission"
        const val _Position = "Position"
        const val _Photo = "photo" // 이미지 필드
        const val _Status = "status"
    } // SQLite의 데이터 타입은 NULL, INTEGER, REAL, TEXT, BLOB 만 지원한다.
}
 



ContactDBHelper.kt

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.provider.BaseColumns
import android.util.Log

// 싱글톤 패턴 구현 시, 해당 클래스의 생성자는 private 로 선언하여 외부에서의 직접 접근을 막아야 한다.
class ContactDBHelper private constructor(context: Context) :
    SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    private val TAG = this.javaClass.simpleName
    override fun onCreate(db: SQLiteDatabase) {
        // db.execSQL(String query) 는 입력한 쿼리문을 실행하는 메소드.
        db.execSQL(SQL_CREATE_ENTRIES)
        Log.v(TAG, "DB Created")
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        // onUpgrade 콜백 메서드는 DB의 스키마가 변경되어 DB 버전이 올라갔을 때 호출된다.
        // DB의 스키마가 변경되면 DB의 버전을 올려야 한다.
        db.execSQL(SQL_DELETE_ENTRIES)
        onCreate(db) //새로 생성하기
    }

    companion object {
        // 먼저 db 파일을 만들어야 한다. db 파일에 관련 테이블들을 생성 및 저장
        private const val DATABASE_VERSION = 1 // 데이터베이스의 버전. 스키마가 변경될 때 숫자를 올린다.
        private const val DATABASE_NAME = "orgChart.db"

        // 테이블을 생성하는 쿼리
        private val SQL_CREATE_ENTRIES = "create table ${ContactContract.Entry.TABLE_NAME} (" +
                "${BaseColumns._ID } INTEGER PRIMARY KEY AUTOINCREMENT," +
                "${ContactContract.Entry._IDX} TEXT not null unique," +
                "${ContactContract.Entry._NAME} TEXT not null," +
                "${ContactContract.Entry._MobileNO} TEXT not null," +
                "${ContactContract.Entry._telNO} TEXT," +
                "${ContactContract.Entry._Team} TEXT," +
                "${ContactContract.Entry._Mission} TEXT," +
                "${ContactContract.Entry._Position} TEXT," +
                "${ContactContract.Entry._Photo} TEXT," +
                "${ContactContract.Entry._Status} TEXT);"

        // SQLite의 데이터 타입은 NULL, INTEGER, REAL, TEXT, BLOB 만 지원한다.
        // DB 생성 위치 : /data/data/<application-package-name>/databases/<database-name>
        private const val SQL_DELETE_ENTRIES =
            "DROP TABLE IF EXISTS ${
ContactContract.Entry.TABLE_NAME}"

        // db 헬퍼는 싱글톤 패턴으로 구현하는 것이 좋다.
        // 싱글톤 패턴은 프로그램 내에서 객체가 1개로 고정되게 하는 패턴.
        private var sInstance: ContactDBHelper? = null

        // 싱글톤 패턴을 구현할 때, 주로 메소드를 getInstance 로 명명한다.
        // 여러 곳에서 동시에 db 를 열면 동기화 문제가 생길 수 있기 때문에 synchronized 키워드를 이용한다.
        @Synchronized
        fun getInstance(context: Context): ContactDBHelper? {
            if (sInstance == null) {  // 객체가 없을 경우에만 객체를 생성한다.
                sInstance = ContactDBHelper(context)
            }
            return sInstance // 객체가 이미 존재할 경우, 기존 객체를 리턴.
        }
    }
}
 


전체 소스코드는 Github에 올려두었다.

https://github.com/jsk005/KotlinProjects/tree/master/sqlite


블로그 이미지

Link2Me

,
728x90

Activity간에 Data를 묶어서 전달해야 할 경우가 있다.

자바로 구현한 예제를 코틀린으로 변환하면 일단 에러가 난다.

import android.os.Parcel;
import android.os.Parcelable;

public class Profiles implements Parcelable {
    // Parcelable 인터페이스 : 커스텀 클래스나 오브젝트를 다른 컴포넌트에 전달해 줘야 할 경우
    // A Activity에서 B Activity로 데이터를 전달할 때, 데이터를 묶어서 전달한다
    // https://developer.android.com/reference/android/os/Parcelable 참조
    // Android에서 성능적인 이슈로 Serializable 보다는 Parcelable를 사용하는걸 추천한다.

    String name;
    int age;
    String gender;

    public Profiles() {
        // 모든 클래스는 반드시 생성자를 가져야 한다.
        // 생성자가 하나도 없을 때만, 컴파일러가 default constructor(기본 생성자) 자동 추가
        // 기본 생성자 : 매개변수가 없는 생성자
    }

    public Profiles(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    //Parcel은 데이터를 전달할 때 사용되는 객체
    //Parcel 데이터를 읽어서 변수에 할당
    public Profiles(Parcel src) {
        name = src.readString();
        age = src.readInt();
        gender = src.readString();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flag) {
        dest.writeString(name);
        dest.writeInt(age);
        dest.writeString(gender);
    }

    public static final Creator<Profiles> CREATOR = new Creator<Profiles>() {

        @Override
        public Profiles createFromParcel(Parcel parcel) {
            return new Profiles(parcel);
        }

        @Override
        public Profiles[] newArray(int size) {
            //배열 객체 만들어 주기
            return new Profiles[size];
        }
    };
}
 


Android Studio가 제공하는 파일 변환 기능으로 코틀린으로 변환하고 약간 수작업으로 수정한 예제

import android.os.Parcel
import android.os.Parcelable
import kotlinx.android.parcel.Parceler
import kotlinx.android.parcel.Parcelize

@Parcelize
class Profiles() : Parcelable {
    // Parcelable 인터페이스 : 커스텀 클래스나 오브젝트를 다른 컴포넌트에 전달해 줘야 할 경우
    // A Activity에서 B Activity로 데이터를 전달할 때, 데이터를 묶어서 전달한다
    // Android에서 성능적인 이슈로 Serializable 보다는 Parcelable를 사용하는걸 추천한다.
    var name: String? = null
    var age = 0
    var gender: String? = null

    constructor(name: String?, age: Int, gender: String?) : this() {
        this.name = name
        this.age = age
        this.gender = gender
    }

    // Parcel은 데이터를 전달할 때 사용되는 객체
    // Parcel 데이터를 읽어서 변수에 할당
    constructor(parcel: Parcel) : this() {
        parcel.run {
            name = readString()
            age = readInt()
            gender = readString()
        }
    }

    override fun describeContents(): Int = 0

    companion object : Parceler<Profiles> {
        override fun Profiles.write(parcel: Parcel, flag: Int) {
            parcel.writeString(name)
            parcel.writeInt(age)
            parcel.writeString(gender)
        }

        override fun create(parcel: Parcel): Profiles {
            return Profiles(parcel)
        }
    }
}
 



그런데 이런 변환 과정을 할 필요없이 아주 간단하게 해결할 수 있다.

https://developer.android.com/kotlin/parcelize 를 참조하자.


앱 build.gradle 추가사항

androidExtensions {
    experimental = true
}


@Parcelize annotation을 사용하면, writeToParcel()/createFromParcel()를 자동으로 구현해준다.

아래 코드를 보라. 얼마나 간단한가.

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class Profiles(
    var name: String="",
    var age:Int = 0,
    var gender: String=""
):Parcelable
 



Activity 이동전

val intent = Intent(this@MainActivity, TargetActivity::class.java)
intent.putExtra("parcel", profile)
startActivity(intent)


Activity 이동후

val profile: Profiles = intent.getParcelableExtra("parcel") 



블로그 이미지

Link2Me

,
728x90

ADB를 활용한 스마트폰 원격 연결방법

 

1. 먼저 PC와 스마트폰간에 USB 케이블로 연결한다.

2. CMD 콘솔창을 띄워서

   adb tcpip 9000    // 임의의 포트 설정

 

3. 스마트폰의 Wi-Fi 를 활성화 시킨다.

4. 스마트폰의 사설 IP주소를 알 수 있게 앱 스토어에서 WiFi Analyzer 앱을 설치한다.

   앱을 실행하면 사설 IP주소를 확인할 수 있다.

   다른 방법으로는 IPTime 공유기에서 확인할 수도 있다.

 

제 스마트폰의 접속 IP주소를 확인해보니 192.168.1.13 이 잡혀 있다.

 

5. adb connect [스마트폰의 연결된 와이파이 ip주소]:[위에서 지정한 포트번호]

    adb connect 192.168.1.13:9000

   

 

연결 해제시에는 adb disconnect 를 하면 된다.

 

 

블로그 이미지

Link2Me

,
728x90


ADB(Android Debug Bridge)는 PC/노트북에서 안드로이드 단말로 명령을 내릴 수 있게 도와주는 도구이다.


1. usb 디버깅 허용 후 단말을 PC와 연결한다.

2. CMD 창에서 아래 명령어들을 입력한다.


ㅇ에뮬레이터 또는 단말 연결을 확인하는 명령어 : adb devies


ㅇ 안드로이드 버전 확인 : adb shell getprop ro.build.version.release


ㅇ 단말 재부팅 : adb reboot


ㅇ APK 설치 : adb install -r [파일명].apk


ㅇ APK 삭제 : adb uninstall [패키지명]




ADB PATH 설정 방법


윈도우 CMD 창을 열고 adb.exe 를 실행해본다.

명령이 실행되지 않으니 adb.exe 경로를 찾아서 윈도우 시스템 환경변수에 등록을 해야 한다.


윈도우키 + Pause 키를 누르면 ....





여기까지 하고 확인 확인으로 창을 닫아주면 PATH 설정은 완료되었다.


이제 CMD 창에서 다시 adb 명령어를 실행해보자.

아래와 같은 화면이 출력되면 PATH 설정이 완료된 것이다.


안드로이드 스튜디오에서도 명령이 잘되는지 실행해 보자.

만약 실행이 안된다면 안드로이드 스튜디오를 재실행하면 될 것이다.


블로그 이미지

Link2Me

,
728x90

https://link2me.tistory.com/1898 게시글은 ViewPager와 Fragment 함께쓰기 예제에 대한 사항이다.

동일 코드 중심으로 ViewPager2 와 Fragment 함께쓰기로 변경했을 경우에 대한 사항을 적어둔다.


https://developer.android.com/training/animation/vp2-migration?hl=ko 게시글을 먼저 읽어보면 도움된다.


기존의 ViewPager 와 변경된 점은 RTL 슬라이딩(좌에서 우로) 지원, 수직방향 슬라이드 지원, 기존 ViewPager의 notifyDataSetChanged 버그 문제해결, offscreenPageLimit를 통한 뷰 계층에 저장된 뷰나 프레그먼트를 제어할 수 있다.


Fragment를 사용하기 위해 FragmentPagerAdapter나 FragmentStatePagerAdapter대신 FragmentStateAdapter를 사용해야 한다.



Layout 에서의 변경사항


앱 build.gradle 추가사항

dependencies {

    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'com.google.android.material:material:1.2.1'
    // ViewPager2
    implementation 'androidx.viewpager2:viewpager2:1.0.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
}
 


ViewPagerActivity.java 변경사항

기존 ViewPager 코드

    private ViewPager mViewPager;
    private MyViewPagerAdapter myPagerAdapter;
    private TabLayout tabLayout;
 

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        Fragment frag1 = new HomeFragment().newInstance(code,"");
        Fragment frag2 = new WebviewFragment().newInstance(code,"https://m.naver.com");
        Fragment frag3 = new AddressFragment().newInstance(code);


        mViewPager = findViewById(R.id.viewPager);
        tabLayout = findViewById(R.id.tab_layout);
        mViewPager.setOffscreenPageLimit(5);

        myPagerAdapter = new MyViewPagerAdapter(getSupportFragmentManager());
        myPagerAdapter.addFrag(frag1,"Home");
        myPagerAdapter.addFrag(frag2,"공지");
        myPagerAdapter.addFrag(frag3,"지도");

        mViewPager.setAdapter(myPagerAdapter);
        tabLayout.setupWithViewPager(mViewPager);

   }

ViewPager2 코드

    private ViewPager2 mViewPager;
    private MyViewPagerAdapter myPagerAdapter;
    private TabLayout tabLayout;

    private String[] titles = new String[]{"리스트", "웹뷰", "연락처"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        Fragment frag1 = new HomeFragment().newInstance(code,"");
        Fragment frag2 = new WebviewFragment().newInstance(code,"https://m.naver.com");
        Fragment frag3 = new AddressFragment().newInstance(code);

        mViewPager = findViewById(R.id.viewPager);
        tabLayout = findViewById(R.id.tab_layout);

        myPagerAdapter = new MyViewPagerAdapter(this);
        myPagerAdapter.addFrag(frag1);
        myPagerAdapter.addFrag(frag2);
        myPagerAdapter.addFrag(frag3);

        mViewPager.setAdapter(myPagerAdapter);

        //displaying tabs
        new TabLayoutMediator(tabLayout, mViewPager, (tab, position) -> tab.setText(titles[position])).attach();
    }
 


MyViewPagerAdapter.java

public class MyViewPagerAdapter extends FragmentStateAdapter {
    private final List<Fragment> mFragmentList = new ArrayList<>();

    public MyViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
        super(fragmentActivity);
    }

    public void addFrag(Fragment fragment) {
        mFragmentList.add(fragment);
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        // Fragment 교체를 보여주는 처리 구현
        return mFragmentList.get(position);
    }

    @Override
    public int getItemCount() {
        // ViewPager 로 보여줄 View의 전체 개수
        return mFragmentList.size();
    }

}


전체 코드는 https://github.com/jsk005/JavaProjects/tree/master/viewpager2 에 올려 두었다.


참고하면 도움되는 자료

https://www.androidhive.info/2020/01/viewpager2-pager-transformations-intro-slider-pager-animations-pager-transformations/



'안드로이드 > Layout' 카테고리의 다른 글

CardView tools:visibility  (0) 2021.08.12
android 검색 + 버튼 배치  (0) 2021.03.14
하단 TabLayout + ViewPager Layout 예제  (0) 2020.11.01
LinearLayout 예제  (0) 2020.10.13
LinearLayout 동적 생성  (0) 2020.10.01
블로그 이미지

Link2Me

,
728x90

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


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









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



블로그 이미지

Link2Me

,
728x90

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

 

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

 

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

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

 

 

 

API 및 서비스를 선택한다.

 

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

 

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

 

 

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

 

 

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

 

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

 

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

 

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

 

 

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

 

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

 

 

 

 

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

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

 

 

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

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

 

 

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

 

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

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

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

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

블로그 이미지

Link2Me

,
728x90

Person 클래스를 ArrayList 로 입력하고, HashSet 을 이용하여 데이터 중복을 제거하고 정렬하는 예제이다.

import java.util.*;
 
public class ArrayList_Person_DupRemove {
    public static void main(String[] args) {
        ArrayList<Person> arr = new ArrayList<>();
        arr.add(new Person("홍길동",25));
        arr.add(new Person("이순신",35));
        arr.add(new Person("강감찬",42));
        arr.add(new Person("김수희",68));
        arr.add(new Person("홍길동",25));
        arr.add(new Person("강감찬",42));
        arr.add(new Person("남진",75));
        arr.add(new Person("나훈아",72));
        arr.add(new Person("홍길동",29));
 
        // HashSet(Set인터페이스를 구현한 대표적인 컬렉션 클래스)을 사용하여 중복 제거하기
        Set<Person> arr2 = new HashSet<>(arr);
        ArrayList<Person> resArr2 = new ArrayList<>(arr2);
 
        System.out.println("중복값 제거후 결과값(순서없음) : " + resArr2.toString());
 
        // 추가한 순서를 유지하며 중복 제거하려면 LinkedHashSet클래스를 사용
        Set<Person> arr3 = new LinkedHashSet<>(arr);
        ArrayList<Person> resArr3 = new ArrayList<>(arr3);
 
        System.out.println("중복값 제거후 결과값(저장순서) : " + resArr3.toString());
 
        // ArrayList 정렬 시 Collections.sort() 메소드를 사용한다.
        Collections.sort(resArr2);
        System.out.println("중복값 제거후 결과값(정렬순서) : " + resArr2.toString());
 
        Collections.sort(resArr3);
        System.out.println("중복값 제거후 결과값(정렬순서) : " + resArr3.toString());
 
        // 역순 정렬
        Collections.sort(resArr2,new Descending());
        System.out.println("중복값 제거후 결과값(역순정렬) : " + resArr2.toString());
 
        Collections.sort(resArr3,new Descending());
        System.out.println("중복값 제거후 결과값(역순정렬) : " + resArr3.toString());
    }
}
 
class Descending implements Comparator {
    // 정렬 : 두 대상 비교하여 자리바꿈 반복
    @Override
    public int compare(Object o1, Object o2) {
        if(o1 instanceof Comparable && o2 instanceof Comparable) {
            Comparable c1 = (Comparable)o1;
            Comparable c2 = (Comparable)o2;
            return c1.compareTo(c2)* -1// -1을 곱해서 기본 정렬방식을 역으로 변경한다.
        }
        return -1;
    }
}
 
// equals()와 hashCode()를 오버라이딩해야 HashSet이 바르게 동작한다.
class Person implements Comparable<Person> {
    // 객체 정렬에 필요한 메소드(정렬기준 제공)를 정의한 인터페이스
    // Comparable : 기본 정렬기준을 구현하는데 사용
    // Comparator : 기준 정렬기준 외에 다른 기준으로 정렬하고자 할 때 사용
    String name;
    int age;
 
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public String toString() {
        return name + ":" + age;
    }
 
    @Override
    public int hashCode() {
        // int hash(Object... values); // 가변인자
        return Objects.hash(name,age);
        // 동일 객체에 대해 hashCode()를 여러 번 호출해도 동일한 값을 반환해야 한다.
    }
 
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Person)) return false;
 
        Person p = (Person) obj;
        // 나 자신(this)의 이름과 나이를 p와 비교
        return this.name.equals(p.name) && this.age == p.age;
        // equals()로 비교해서 true를 얻은 두 객체의 hashCode()값은 일치해야 한다.
    }
 
    // Comparable 인터페이스를 상속받아 compareTo() 함수를 오버라이딩
    @Override
    public int compareTo(Person p) {
        // 주어진 객체(p)를 자신(this)과 비교
        return this.name.compareTo(p.name);
    }
 
}
 
 

 

실행결과

 

ArrayList 로 입력한 데이터를 중복제거하고, 정렬하는 간단한 예제이다.

출처: https://link2me.tistory.com/1912 [소소한 일상 및 업무TIP 다루기]

나이순 정렬 기능 추가

import java.util.*;
 
public class ArrayList_Person_DupRemove {
    public static void main(String[] args) {
        ArrayList<Person> arr = new ArrayList<>();
        arr.add(new Person("홍길동",25));
        arr.add(new Person("이순신",35));
        arr.add(new Person("강감찬",42));
        arr.add(new Person("김수희",68));
        arr.add(new Person("홍길동",25));
        arr.add(new Person("강감찬",42));
        arr.add(new Person("남진",75));
        arr.add(new Person("나훈아",72));
        arr.add(new Person("홍길동",29));
 
        // HashSet(Set인터페이스를 구현한 대표적인 컬렉션 클래스)을 사용하여 중복 제거하기
        Set<Person> arr2 = new HashSet<>(arr);
        ArrayList<Person> resArr2 = new ArrayList<>(arr2);
 
        System.out.println("중복값 제거후 결과값(순서없음) : " + resArr2.toString());
 
        // 추가한 순서를 유지하며 중복 제거하려면 LinkedHashSet클래스를 사용
        Set<Person> arr3 = new LinkedHashSet<>(arr);
        ArrayList<Person> resArr3 = new ArrayList<>(arr3);
 
        System.out.println("중복값 제거후 결과값(저장순서) : " + resArr3.toString());
 
        // ArrayList 정렬 시 Collections.sort() 메소드를 사용한다.
        Collections.sort(resArr2);
        System.out.println("중복값 제거후 결과값(정렬순서) : " + resArr2.toString());
 
        Collections.sort(resArr3);
        System.out.println("중복값 제거후 결과값(정렬순서) : " + resArr3.toString());
 
        // 역순 정렬
        Collections.sort(resArr2,new Descending());
        System.out.println("중복값 제거후 결과값(역순정렬) : " + resArr2.toString());
 
        Collections.sort(resArr3,new Descending());
        System.out.println("중복값 제거후 결과값(역순정렬) : " + resArr3.toString());
 
        // 나이순 정렬
        Collections.sort(resArr2,(a, b) -> Person.AgeCompareTo(a,b));
        System.out.println("중복값 제거후 결과값(나이순서) : " + resArr2.toString());
 
        Collections.sort(resArr2,(a, b) -> Person.DescAgeCompareTo(a,b));
        System.out.println("중복값 제거후 결과값(나이역순) : " + resArr2.toString());
    }
}
 
class Descending implements Comparator {
    // 정렬 : 두 대상 비교하여 자리바꿈 반복
    @Override
    public int compare(Object o1, Object o2) {
        if(o1 instanceof Comparable && o2 instanceof Comparable) {
            Comparable c1 = (Comparable)o1;
            Comparable c2 = (Comparable)o2;
            return c1.compareTo(c2)* -1// -1을 곱해서 기본 정렬방식을 역으로 변경한다.
        }
        return -1;
    }
}
 
// equals()와 hashCode()를 오버라이딩해야 HashSet이 바르게 동작한다.
class Person implements Comparable<Person> {
    // 객체 정렬에 필요한 메소드(정렬기준 제공)를 정의한 인터페이스
    // Comparable : 기본 정렬기준을 구현하는데 사용
    // Comparator : 기준 정렬기준 외에 다른 기준으로 정렬하고자 할 때 사용
    String name;
    int age;
 
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public String toString() {
        return name + ":" + age;
    }
 
    @Override
    public int hashCode() {
        // int hash(Object... values); // 가변인자
        return Objects.hash(name,age);
        // 동일 객체에 대해 hashCode()를 여러 번 호출해도 동일한 값을 반환해야 한다.
    }
 
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Person)) return false;
 
        Person p = (Person) obj;
        // 나 자신(this)의 이름과 나이를 p와 비교
        return this.name.equals(p.name) && this.age == p.age;
        // equals()로 비교해서 true를 얻은 두 객체의 hashCode()값은 일치해야 한다.
    }
 
    // Comparable 인터페이스를 상속받아 compareTo() 함수를 오버라이딩
    @Override
    public int compareTo(Person p) {
        // 주어진 객체(p)를 자신(this)과 비교
        return this.name.compareTo(p.name);
    }
 
    // 나이순 정렬
    public static int AgeCompareTo(Person p1, Person p2){
        if(p1.age > p2.age) return 1;
        else if(p1.age < p2.age) return -1;
        else return 0;
    }
 
    // 나이 역순 정렬
    public static int DescAgeCompareTo(Person p1, Person p2){
        if(p1.age < p2.age) return 1;
        else if(p1.age > p2.age) return -1;
        else return 0;
    }
 
}
 

 

불필요한 코드를 정리한 예제

import java.util.*;
 
public class ArrayList_Person_DupRemove {
    public static void main(String[] args) {
        ArrayList<Person> arr = new ArrayList<>();
        arr.add(new Person("홍길동",25));
        arr.add(new Person("이순신",35));
        arr.add(new Person("강감찬",42));
        arr.add(new Person("김수희",68));
        arr.add(new Person("홍길동",25));
        arr.add(new Person("강감찬",42));
        arr.add(new Person("남진",75));
        arr.add(new Person("나훈아",72));
        arr.add(new Person("홍길동",29));
        arr.add(new Person("유재석",25));
 
        // HashSet(Set인터페이스를 구현한 대표적인 컬렉션 클래스)을 사용하여 중복 제거하기
        Set<Person> arr2 = new HashSet<>(arr);
        ArrayList<Person> resArr2 = new ArrayList<>(arr2);
 
        System.out.println("중복값 제거후 결과값(순서없음) : " + resArr2.toString());
 
        // 추가한 순서를 유지하며 중복 제거하려면 LinkedHashSet클래스를 사용
        Set<Person> arr3 = new LinkedHashSet<>(arr);
        ArrayList<Person> resArr3 = new ArrayList<>(arr3);
 
        System.out.println("중복값 제거후 결과값(저장순서) : " + resArr3.toString());
 
        // ArrayList 정렬 시 Collections.sort() 메소드를 사용한다.
        Collections.sort(resArr2);
        System.out.println("중복값 제거후 결과값(정렬순서) : " + resArr2.toString());
 
        Collections.sort(resArr3);
        System.out.println("중복값 제거후 결과값(정렬순서) : " + resArr3.toString());
 
 
        // 나이순 정렬
        Collections.sort(resArr2,Comparator.comparing(Person::getAge) // 나이순정렬
                .thenComparing(Comparator.naturalOrder())); // 기본정렬
        System.out.println("중복값 제거후 결과값(나이정렬) : " + resArr2.toString());
 
        // 역순 정렬
        Collections.sort(resArr2,Comparator.comparing(Person::getAge).reversed());
        System.out.println("중복값 제거후 결과값(나이역순) : " + resArr2.toString());
 
    }
}
 
// equals()와 hashCode()를 오버라이딩해야 HashSet이 바르게 동작한다.
class Person implements Comparable<Person> {
    // 객체 정렬에 필요한 메소드(정렬기준 제공)를 정의한 인터페이스
    // Comparable : 기본 정렬기준을 구현하는데 사용
    // Comparator : 기준 정렬기준 외에 다른 기준으로 정렬하고자 할 때 사용
    String name;
    int age;
 
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public String getName() {
        return name;
    }
 
    public int getAge() {
        return age;
    }
 
    public String toString() {
        return name + ":" + age;
    }
 
    @Override
    public int hashCode() {
        // int hash(Object... values); // 가변인자
        return Objects.hash(name,age);
        // 동일 객체에 대해 hashCode()를 여러 번 호출해도 동일한 값을 반환해야 한다.
    }
 
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Person)) return false;
 
        Person p = (Person) obj;
        // 나 자신(this)의 이름과 나이를 p와 비교
        return this.name.equals(p.name) && this.age == p.age;
        // equals()로 비교해서 true를 얻은 두 객체의 hashCode()값은 일치해야 한다.
    }
 
    // Comparable 인터페이스를 상속받아 compareTo() 함수를 오버라이딩
    @Override
    public int compareTo(Person p) {
        // 주어진 객체(p)를 자신(this)과 비교
        return this.name.compareTo(p.name);
    }
 
}
 
 

 

'안드로이드 > Java 문법' 카테고리의 다른 글

[Java] Generics 예제1  (2) 2021.11.10
HashMap 예제  (0) 2021.10.30
[Java] HashSet 를 이용한 ArrayList 중복제거 및 단순 정렬  (0) 2020.12.12
[Java] HashSet 예제  (0) 2020.12.12
Java 와 C/C++ 비교  (0) 2020.11.08
블로그 이미지

Link2Me

,
728x90

ArrayList 로 입력한 데이터를 중복제거하고, 정렬하는 간단한 예제이다.

 

import java.util.*;
 
public class ArrayList_Remove_Dup {
    public static void main(String[] args) {
        ArrayList<String> arr = new ArrayList<String>();
        arr.add("홍길동");
        arr.add("강감찬");
        arr.add("이순신");
        arr.add("김두환");
        arr.add("홍길동");
        arr.add("이순신");
        arr.add("이순신");
        arr.add("권율");
 
        // HashSet(Set인터페이스를 구현한 대표적인 컬렉션 클래스)을 사용하여 중복 제거하기
        HashSet<String> arr2 = new HashSet<String>(arr);
        ArrayList<String> resArr2 = new ArrayList<String>(arr2);
 
        System.out.println("중복값 제거후 결과값(순서없음) : " + resArr2.toString());
 
        // 추가한 순서를 유지하며 중복 제거하려면 LinkedHashSet클래스를 사용
        Set<String> arr3 = new LinkedHashSet<>(arr);
        ArrayList<String> resArr3 = new ArrayList<String>(arr3);
 
        System.out.println("중복값 제거후 결과값(저장순서) : " + resArr3.toString());
 
        // ArrayList 정렬 시 Collections.sort() 메소드를 사용한다.
        Collections.sort(resArr2);
 
        System.out.println("중복값 제거후 결과값(정렬순서) : " + resArr2.toString());
    }
}
 

 

실행결과

 

'안드로이드 > Java 문법' 카테고리의 다른 글

HashMap 예제  (0) 2021.10.30
[Java] HashSet 를 이용한 ArrayList 중복제거 및 정렬  (0) 2020.12.12
[Java] HashSet 예제  (0) 2020.12.12
Java 와 C/C++ 비교  (0) 2020.11.08
[Java] 람다식(Lambda Expression)  (0) 2020.07.26
블로그 이미지

Link2Me

,
728x90

HashSet : 중복해서 저장하지 않는 집합(Set)으로 사용할 수 있는 클래스

HashSet은 객체를 저장하기 전에 기존에 같은 객체가 있는지 확인한다.

같은 객체가 없으면 저장하고, 있으면 저장하지 않는다.

import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
 
public class HashSetEX {
    public static void main(String[] args) {
        HashSet set = new HashSet();
 
        // HashSet은 객체를 저장하기전에 기존에 같은 객체가 있는지 확인한다.
        // 같은 객체가 없으면 저장하고, 있으면 저장하지 않는다.
        set.add("abc");
        set.add("abc");
 
        // boolean add(Object o)는 저장할 객체의 equals()와 hashCode()를 호출
        // equals()와 hashCode()가 오버라이딩 되어 있어야 한다.
        set.add(new PerSon("홍길동",33));
        set.add(new PerSon("이순신",35));
        set.add(new PerSon("홍길동",33));
 
        System.out.println(set);
 
        // Iterator : 컬렉션에 저장된 요소들을 읽어오는 방법을 표준화한 것
        Iterator iterator = set.iterator(); // 값을 꺼내는 메소드를 이용한다.
        while (iterator.hasNext()) { // 읽어올 요소가 남아 있는가?
            System.out.println(iterator.next()); // 다음 요소를 읽어온다.
        }
        // HashSet는 데이터에 순서가 없기 때문에 데이터를 순서대로 읽어오거나,
        // 특정 위치의 데이터를 읽어올 수 있는 방법이 없기 때문에
        // "Iterator" 메소드를 활용해서 집합에 있는 "전체" 데이터를 불러올 수 있다.
    }
 
}
 
// equals()와 hashCode()를 오버라이딩해야 HashSet이 바르게 동작한다.
class PerSon {
    String name;
    int age;
 
    public PerSon(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public String toString() {
        return name + ":" + age;
    }
 
    @Override
    public int hashCode() {
        // int hash(Object... values); // 가변인자
        return Objects.hash(name,age);
        // 동일 객체에 대해 hashCode()를 여러 번 호출해도 동일한 값을 반환해야 한다.
    }
 
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof PerSon)) return false;
 
        PerSon p = (PerSon) obj;
        // 나 자신(this)의 이름과 나이를 p와 비교
        return this.name.equals(p.name) && this.age == p.age;
        // equals()로 비교해서 true를 얻은 두 객체의 hashCode()값은 일치해야 한다.
    }
 
}
 

 

 

실행결과

 

블로그 이미지

Link2Me

,
728x90

보통 WebView는 서버에 있는 html 파일이나 PHP 등의 파일을 읽어서 보여주는 기능을 한다.

License.html 와 같은 파일은 앱에 직접 경로를 지정하여 읽어들이면 좋다.




아래와 같이 assets 폴더가 생성되었다.

이제 여기에 license.html 파일을 복사해서 넣는다.

물론 html 파일 하나로 css, javascript, html 모두 포함되도록 작성하는 게 좋다.

분리해서 파일을 작성할 경우에는 같은 폴더에 파일이 존재해야 한다.



WebView 에서 경로 지정은 어떻게 해야 할까?

아래 색상 표시한 것이 assets 폴더를 인식하는 URL 이다.

WebView webView = findViewById(R.id.webView);
webView.loadUrl("file:///android_asset/license.html"); 


'안드로이드 > Android 활용' 카테고리의 다른 글

Android APP(Java)에서 PHP Session 저장 및 사용  (0) 2023.12.23
PDF Viewer 구현 예제(Java Code)  (0) 2022.06.05
Android TextToSpeech  (0) 2020.09.30
Intent 이메일 전송하기  (0) 2020.09.08
Profile 이미지 처리  (0) 2020.09.01
블로그 이미지

Link2Me

,
728x90

난독화란?

앱을 컴파일하여 만들어진 apk 파일을 역컴파일 툴을 이용하면 소스코드가 대부분 보인다.

물론 100% 정확하게 보이는 것은 아니지만, 이런 코드를 사용했고, key 값 등이 노출될 수 있다.

그래서 이런 것을 방지하기 위해 코드를 난독화하는 솔루션을 이용하여 난독화 적용한 앱을 만든다.

이렇게 만들어진 코드는 분석하기가 난해해진다.

 

 

Your APK size is going to be larger when extractNativeLibs is set to false.

Android Studio에서 사용하는 gradle 플러그인의 기본값이 gradle 버전 3.6.0을 기준으로 false로 변경되었다.

extractNativeLibs 값이 true인 경우에만 정상적으로 설치되는것을 확인했다.

 

안드로이드 1차 난독화 파일로 2차 난독화를 위한 파일 생성을 받은 후에 1차 난독화시에 적용했던 서명이 풀린다고 한다. 그래서 Android Studio 를 이용한 서명 자동 추가가 아니라 CMD 명령어로 서명을 직접 추가하는 작업을 해야 한다.

 

zipalign -p -f -v 4 unsigned_CarMail.apk sCarMail.apk
apksigner sign --ks CarMailSender.jks --ks-key-alias carmailsender sCarMail.apk
apksigner verify --print-certs sCarMail.apk

위와 같은 과정으로 생성된 APK 파일을 실행했을 때 설치 실패가 된다면

즉 [INSTALL_FAILED_INVALID_APK] 라면 android:extractNativeLibs="true"

를 추가하면 해결될 것이다.

 

scanrisk 함수를 추가한 경우 난독화 제외 처리를 해주어야 한다.

 

블로그 이미지

Link2Me

,