AndroidStudio로 APK만들기 (계획짜기)

2024. 6. 3. 12:03컴퓨터과학

반응형

*계속 수정중입니다 (현재 24.06.10.)

 

 

* 개발 개요

더보기

- 개발 할 앱('앱A'로 명명)

- 기존에 사용하고있는 앱('앱B'로 명명)

- '앱A'에는 백그라운드에서 실행된다. 즉, 사용자가 직접 앱을 켤 필요가 없다.

- '앱B'에서 특정 TextBox에 입력된 텍스트가 있다면 '앱A'가 그 분석 결과를 팝업형태로 '앱B'에서 띄운다.

- 단, '앱B'는 어떠한 개발도 진행할 수 없는 외부 사용 앱이다.

- '앱B'에 접근하여 데이터를 읽어 '앱A'에서 작업하고 '앱B'에서 출력하는 방식의 작업이며,

이것은 ' AccessibilityService를 사용한다.

 


* 작업 순서

1. 앱B에서 앱A와 상호작용하기 위한 작업

ㆍAndroidStudio 실행

ㆍ 새 프로젝트 생성 > 'Empty Activity' > 설정 : Language = Kotlin

ㆍ파일 종속성 추가

    - app/build.gradle

더보기
plugins {
    id("com.android.application")
    id("kotlin-android")
}

android {
    namespace = "com.example.newapplication"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.newapplication"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = 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"
    }
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.1"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

dependencies {
    implementation("androidx.core:core-ktx:1.8.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
    implementation("androidx.activity:activity-compose:1.3.1")
    implementation(platform("androidx.compose:compose-bom:2023.01.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.3")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
    androidTestImplementation(platform("androidx.compose:compose-bom:2023.01.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

    // 추가된 라이브러리
    implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.31")
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
    implementation("com.github.bumptech.glide:glide:4.12.0")
    implementation("org.pytorch:pytorch_android_lite:1.9.0")
    implementation("org.pytorch:pytorch_android_torchvision:1.9.0")

    // TensorFlow Lite 라이브러리 추가
    implementation("org.tensorflow:tensorflow-lite:2.4.0")
    implementation("org.tensorflow:tensorflow-lite-support:0.1.0")
}

 

ㆍ Accessibility Service 선언

     - app\src\main\AndroidManifest.xml

더보기

* 다른 어플리케이션에 접근해 UI를 읽어오기 위한 기능

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.NewApplication"
        tools:targetApi="31">
        
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.NewApplication">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- MyAccessibilityService 추가 -->
        <service
            android:name=".MyAccessibilityService"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config" />
        </service>
        <!-- MyAccessibilityService 추가 끝 -->
        
    </application>

</manifest>

     - res/xml/accessibility_service_config.xml 파일 생성

더보기
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowContentChanged|typeViewTextChanged"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:notificationTimeout="100"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/accessibility_service_description"
    android:packageNames="com.company.appB" />

     - app/src/main/java/MyAccessibilityService.kt클래스 생성

더보기
package com.example

import android.accessibilityservice.AccessibilityService
import android.os.Handler
import android.os.Looper
import android.view.accessibility.AccessibilityEvent
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.PopupWindow
import android.widget.TextView
import android.view.Gravity

class MyAccessibilityService : AccessibilityService() {

    private lateinit var bertModel: BertModel
    private val handler = Handler(Looper.getMainLooper()) // 타이머를 위한 Handler
    private var runnable: Runnable? = null
    private var lastInputText: String = ""

    override fun onServiceConnected() {
        super.onServiceConnected()
        bertModel = BertModel(assets)
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        val source = event.source ?: return
        val viewId = source.viewIdResourceName

        if (viewId == "com.company.appB:id/text_input_field_id") {
            val text = source.text?.toString() ?: return
            if (text != lastInputText) { // 텍스트가 변경되었을 때만 타이머 시작
                lastInputText = text
                startTimerForClassification(text)
            }
        }
    }

    override fun onInterrupt() {
        // 서비스가 중단되었을 때 처리할 작업
    }

    private fun startTimerForClassification(text: String) {
        // 이전에 설정된 타이머가 있으면 취소
        runnable?.let { handler.removeCallbacks(it) }

        // 새로운 타이머 설정
        runnable = Runnable {
            performClassification(text) // 타이머가 완료되면 분류작업 수행
        }

        // 타이머 시작 (예: 1.2초 대기 후 실행)
        handler.postDelayed(runnable!!, 1200)
    }

    private fun performClassification(text: String) {
        val classificationResult = bertModel.classifyText(text)
        showPopupWindow(classificationResult) // 분류 결과를 팝업으로 표시
    }

    private fun showPopupWindow(result: String) {
        val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val view = inflater.inflate(R.layout.popup_layout, null)

        val popupWindow = PopupWindow(view, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
        val textView = view.findViewById<TextView>(R.id.popup_text)
        textView.text = result

        val rootView = currentFocus ?: window.decorView.rootView
        popupWindow.showAtLocation(rootView, Gravity.CENTER, 0, 0)
    }
}

 

 

     - 팝업 레이아웃 파일 생성 : res/layout/ popup_layout.xml

더보기
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp"
    android:background="@drawable/popup_background">

    <TextView
        android:id="@+id/popup_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="분류 결과"
        android:textSize="18sp"
        android:textColor="@android:color/black" />
</LinearLayout>

     - 팝업 배경 파일 생성 : res/drawable/ popup_background.xml

더보기
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FFFFFF"/>
    <corners android:radius="8dp"/>
    <padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp"/>
    <stroke android:width="1dp" android:color="#CCCCCC"/>
</shape>

 


2. 앱A의 분류기능 작성하기

ㆍassets폴더에 분류기능 모델 넣어두기

     *파일목록

더보기

1. bert_model.tflite

  • 설명: TensorFlow Lite 형식으로 변환된 BERT 모델 파일입니다.
  • 역할: 이 파일은 모델이 Android 장치에서 실행될 때 사용됩니다.

2. vocab.txt

  • 설명: BERT 모델의 토크나이저에 필요한 단어 사전 파일입니다.
  • 역할: 입력 텍스트를 토큰으로 변환하는 데 사용됩니다.

3. config.json

  • 설명: BERT 모델의 구성 파일입니다.
  • 역할: 모델의 설정 정보를 포함하고 있습니다. (TensorFlow Lite에서는 이 파일이 항상 필요하지 않을 수 있습니다.)

4. special_tokens_map.json

  • 설명: 특수 토큰(예: [CLS], [SEP])에 대한 매핑 정보입니다.
  • 역할: 입력 텍스트를 토큰화할 때 필요한 특수 토큰 정보를 제공합니다.

5. tokenizer_config.json

  • 설명: 토크나이저의 구성 파일입니다.
  • 역할: 토크나이저의 설정 정보를 포함하고 있습니다.

6. new_tokens.txt

  • 목적: BERT 모델이 인식하지 못하는 새로운 토큰을 추가할 수 있습니다. 예를 들어, 도메인 특정 용어, 슬랭, 약어 등이 있을 수 있습니다.
  • 사용 방법: 이 파일에 정의된 새로운 토큰을 기존의 토크나이저에 추가하여 모델이 이를 인식할 수 있도록 합니다.

7. synonyms.json

  • 목적: 텍스트 전처리 과정에서 유사어를 통일하여 일관된 입력을 제공할 수 있습니다. 예를 들어, "집"과 "거주지"를 동일한 의미로 처리할 수 있습니다.
  • 사용 방법: 입력 텍스트를 토큰화하기 전에 이 파일을 사용하여 유사어를 대체합니다.

     *파일 구조 예시

더보기

app/
├── src/
│   ├── main/
│   │   ├── assets/
│   │   │   ├── bert_model.tflite
│   │   │   ├── vocab.txt
│   │   │   ├── config.json
│   │   │   ├── special_tokens_map.json
│   │   │   ├── tokenizer_config.json
│   │   │   ├── new_tokens.txt
│   │   │   ├── synonyms.json
│   │   ├── java/
│   │   │   ├── com/
│   │   │   │   ├── example/
│   │   │   │   │   ├── newapplication/
│   │   │   │   │   │   ├── MyAccessibilityService.kt
│   │   │   │   │   │   ├── MainActivity.kt
│   │   │   │   │   │   ├── BertModel.kt (주석처리)
│   │   │   │   │   │   ├── BertTokenizer.kt (주석처리)
│   │   ├── res/
│   │   │   ├── layout/
│   │   │   │   ├── popup_layout.xml
│   │   │   ├── drawable/
│   │   │   │   ├── popup_background.xml

 

ㆍBERT모델(분류기능) 로드 및 초기화

     - BERT모델 로드 / 텍스트 분류 코드 추가  : BertModel.kt

더보기
import android.content.res.AssetManager
import org.tensorflow.lite.Interpreter
import java.io.FileInputStream
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel

class BertModel(private val assetManager: AssetManager) {

    private lateinit var interpreter: Interpreter
    private lateinit var tokenizer: BertTokenizer

    init {
        interpreter = Interpreter(loadModelFile(assetManager, "bert_model.tflite"))
        tokenizer = BertTokenizer(assetManager)
    }

    private fun loadModelFile(assetManager: AssetManager, modelFileName: String): MappedByteBuffer {
        val assetFileDescriptor = assetManager.openFd(modelFileName)
        val fileInputStream = FileInputStream(assetFileDescriptor.fileDescriptor)
        val fileChannel = fileInputStream.channel
        val startOffset = assetFileDescriptor.startOffset
        val declaredLength = assetFileDescriptor.declaredLength
        return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength)
    }

    fun classifyText(inputText: String): String {
        val tokens = tokenizer.tokenize(inputText)
        val inputIds = IntArray(128) { 0 }
        tokens.forEachIndexed { index, token ->
            inputIds[index] = tokenizer.vocab[token] ?: 0
        }

        val output = Array(1) { FloatArray(3) }
        interpreter.run(arrayOf(inputIds), output)

        return "분류 결과: ${output[0].joinToString(", ")}"
    }
}

     - Bert 토크나이저 코드 추가 : BertTokenizer.kt

더보기
import android.content.res.AssetManager
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStreamReader

class BertTokenizer(assetManager: AssetManager) {

    val vocab: Map<String, Int>
    private val newTokens: Set<String>
    private val synonyms: Map<String, String>

    init {
        vocab = loadVocab(assetManager, "vocab.txt")
        newTokens = loadNewTokens(assetManager, "new_tokens.txt")
        synonyms = loadSynonyms(assetManager, "synonyms.json")
    }

    private fun loadVocab(assetManager: AssetManager, vocabFileName: String): Map<String, Int> {
        val vocab = mutableMapOf<String, Int>()
        assetManager.open(vocabFileName).bufferedReader().useLines { lines ->
            lines.forEachIndexed { index, line ->
                vocab[line] = index
            }
        }
        // 새로운 토큰 추가
        newTokens.forEach { token ->
            if (!vocab.containsKey(token)) {
                vocab[token] = vocab.size
            }
        }
        return vocab
    }

    private fun loadNewTokens(assetManager: AssetManager, newTokensFileName: String): Set<String> {
        val newTokens = mutableSetOf<String>()
        assetManager.open(newTokensFileName).bufferedReader().useLines { lines ->
            lines.forEach { line ->
                newTokens.add(line.trim())
            }
        }
        return newTokens
    }

    private fun loadSynonyms(assetManager: AssetManager, synonymsFileName: String): Map<String, String> {
        val synonyms = mutableMapOf<String, String>()
        val json = assetManager.open(synonymsFileName).bufferedReader().use { it.readText() }
        val jsonObject = JSONObject(json)
        jsonObject.keys().forEach { key ->
            synonyms[key] = jsonObject.getString(key)
        }
        return synonyms
    }

    fun tokenize(text: String): List<String> {
        // 유사어 대체
        var processedText = text.split(" ").joinToString(" ") { word ->
            synonyms.getOrDefault(word.toLowerCase(), word)
        }

        // 텍스트를 토큰으로 변환
        return processedText.split(" ").map { it.toLowerCase() }
    }
}

 

 


 

3. app 실행 테스트 전 확인사항

더보기

1. Gradle 설정 확인

  • 프로젝트의 Gradle 설정이 올바른지 확인하세요.
  • File > Sync Project with Gradle Files를 선택하여 Gradle 파일을 동기화합니다.

2. 프로젝트 빌드 확인

  • 프로젝트가 올바르게 빌드되었는지 확인하세요.
  • Build > Make Project를 선택하여 프로젝트를 다시 빌드해보세요.
  • 빌드 오류가 있는지 확인하고, 오류가 있다면 이를 해결합니다.

3. 연결된 디바이스 확인

  • 디바이스가 올바르게 연결되었는지 확인하세요.
  • 디바이스가 USB 디버깅 모드로 설정되어 있는지 확인하세요.
  • Run > Select Device를 선택하여 연결된 디바이스를 선택할 수 있는지 확인합니다. 

추가 확인 사항

  • 프로젝트 구조 확인: 프로젝트의 파일 구조와 설정이 올바른지 확인합니다.
  • 로그 확인: Android Studio의 Logcat에서 발생한 오류 메시지를 확인하여 문제를 해결합니다.

 


 

 

4. 실제 디바이스에서 테스트하기

ㆍ실제 디바이스 테스트

     - USB 디버깅 활성화/디바이스 연결

- Android 디바이스에서 설정 -> 휴대전화 정보 -> 빌드 번호를 여러 번 클릭하여 개발자 모드를 활성화
- 개발자 옵션 -> USB 디버깅을 활성화
- USB 케이블을 사용하여 디바이스를 컴퓨터에 연결
- 디바이스에서 USB 디버깅 허용 요청 허용

 

     - 앱 실행/Accessibility Service 활성화

- Android Studio에서 Run -> Run 'app'을 선택하여 앱을 디바이스에 설치하고 실행
- 연결된 디바이스를 선택하여 앱이 설치되고 실행됨

 

   - Accessibility Service 활성화

- 디바이스의 설정 -> 접근성 -> 설치된 서비스에서 MyAccessibilityService를 활성화

 

    - '앱B' 테스트 

- '앱B'를 실행하여 '현장등록개요' 텍스트 입력창에 텍스트를 입력합니다.
- 입력한 텍스트가 분류 결과로 팝업 형태로 나타나는지 확인합니다.

 

     * '앱B'의 텍스트박스 ID 확인하기

더보기

방법 1: UI Automator Viewer 사용

  1. UI Automator Viewer 설치 및 실행:
    • Android SDK에 포함되어 있습니다. 설치 후 tools/bin 폴더에서 uiautomatorviewer.bat 파일을 실행합니다 (Windows 기준).
  2. 기기 연결 및 앱 실행:
    • USB 디버깅을 활성화한 상태에서 기기를 컴퓨터에 연결하고 '앱B'를 실행합니다.
  3. 스크린샷 캡처:
    • UI Automator Viewer에서 Device Screenshot (uiautomator dump) 버튼을 클릭하여 현재 화면의 스크린샷을 캡처합니다.
  4. UI 요소 확인:
    • 스크린샷에서 텍스트 입력창을 클릭하면 해당 요소의 속성을 확인할 수 있습니다.
    • resource-id 항목이 텍스트 입력창의 ID입니다.

방법 2: Android Studio의 Layout Inspector 사용

  1. 앱 실행:
    • Android Studio에서 디버그 모드로 '앱B'를 실행합니다.
  2. Layout Inspector 실행:
    • Android Studio에서 View > Tool Windows > Layout Inspector를 선택합니다.
  3. 디바이스 선택:
    • 연결된 디바이스를 선택하고, 현재 실행 중인 앱의 화면을 선택합니다.
  4. UI 요소 확인:
    • Layout Inspector 창에서 텍스트 입력창을 선택하면 해당 요소의 ID와 기타 속성을 확인할 수 있습니다.

 

반응형