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 사용
- UI Automator Viewer 설치 및 실행:
- Android SDK에 포함되어 있습니다. 설치 후 tools/bin 폴더에서 uiautomatorviewer.bat 파일을 실행합니다 (Windows 기준).
- 기기 연결 및 앱 실행:
- USB 디버깅을 활성화한 상태에서 기기를 컴퓨터에 연결하고 '앱B'를 실행합니다.
- 스크린샷 캡처:
- UI Automator Viewer에서 Device Screenshot (uiautomator dump) 버튼을 클릭하여 현재 화면의 스크린샷을 캡처합니다.
- UI 요소 확인:
- 스크린샷에서 텍스트 입력창을 클릭하면 해당 요소의 속성을 확인할 수 있습니다.
- resource-id 항목이 텍스트 입력창의 ID입니다.
방법 2: Android Studio의 Layout Inspector 사용
- 앱 실행:
- Android Studio에서 디버그 모드로 '앱B'를 실행합니다.
- Layout Inspector 실행:
- Android Studio에서 View > Tool Windows > Layout Inspector를 선택합니다.
- 디바이스 선택:
- 연결된 디바이스를 선택하고, 현재 실행 중인 앱의 화면을 선택합니다.
- UI 요소 확인:
- Layout Inspector 창에서 텍스트 입력창을 선택하면 해당 요소의 ID와 기타 속성을 확인할 수 있습니다.
'컴퓨터과학' 카테고리의 다른 글
[현장 개발용어 기초개념]현장 담당자가 DevOps를 말하는 의도는 무엇인가 (0) | 2024.06.28 |
---|---|
[안드로이드 앱 생성]PyTorch 모델을 TensorFlow Lite모델로 변환 (0) | 2024.06.03 |
[AndroidStudio]Manifest merger failed with multiple errors, see logsDuplicate class 오류Java heap space 문제 (0) | 2024.05.30 |
AWS로 서비스 배포하기(3) _DB연결하기(PuTTY, sqlDeveloper, eclipse) (0) | 2021.08.19 |
AWS로 서비스 배포하기(2)_.war파일을 AWS서버에 직접 배포하기 (1) | 2021.08.19 |