Skip to content

Conversation

@thdudgus
Copy link
Contributor

Feat # 226

변경사항
앱 차단 기능으로 포커스 모드 구현

  • Android: 방해가 되는 앱을 차단하기 위해 접근성 서비스 구현
  • Android: 메인액티비티 패키지 업데이트 및 MethodChannel 핸들러 추가
  • Flutter: NativeBlockController 추가 및 DeepTimeScreen과 통합
  • UX: 권한 팝업을 추가하고 logic on app switch를 redirect

스크린샷
After
image
해당 팝업을 통해 접근성 설정 페이지 (기기 시스템 설정)로 이동 -> 허용 후엔 북스타 외 다른 앱 차단 기능 활성화

- FocusAccessibilityService를 추가하여 미허용 앱 탐지 및 차단
- 차단 상태를 제어하기 위해 MainActivity에 MethodChannel 구현
- 올바른 메인액티비티 패키지 구조(com.example -> com.company)
- 매니페스트에 접근성 서비스 구성 및 권한 추가
- Android 네이티브 서비스와 통신하기 위해 NativeBlockController 추가
- 접근성 서비스에 대한 권한 안내 팝업 구현
- DeepTimeScreen의 잠금 아이콘 탭에서 차단 로직 시작/정지 트리거
@thdudgus thdudgus self-assigned this Jan 28, 2026
@thdudgus thdudgus added the Feat label Jan 28, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @thdudgus, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 Android 기기에서 사용자가 독서에 집중할 수 있도록 방해되는 다른 애플리케이션의 실행을 차단하는 포커스 모드 기능을 도입합니다. 이를 위해 Android의 접근성 서비스를 활용하여 앱 전환 이벤트를 감지하고, Flutter와 Native 코드 간의 통신 채널을 통해 포커스 모드의 활성화/비활성화 및 필요한 접근성 권한 관리를 수행합니다. 사용자 경험을 고려하여 권한이 없을 경우 설정 페이지로 안내하는 기능도 포함되어 있습니다.

Highlights

  • Android 접근성 서비스 구현: 다른 앱의 실행을 차단하는 'FocusAccessibilityService'를 추가하여 포커스 모드 기능을 구현했습니다.
  • Flutter-Native 통신 채널 구축: Flutter 앱에서 Android의 차단 기능을 제어하고 접근성 권한을 관리하기 위한 MethodChannel을 MainActivity.kt에 설정했습니다.
  • 앱 차단 로직: 포커스 모드 활성화 시, 화이트리스트에 없는 앱이 실행되면 'BookStar' 앱을 전면으로 가져와 사용자의 집중을 돕습니다.
  • 권한 관리 및 UX 개선: ReadingChallengeQuizDeepTimeScreen에 접근성 권한 확인, 요청, 그리고 사용자에게 권한 설정을 안내하는 다이얼로그 로직을 통합했습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이번 PR은 접근성 서비스를 사용하여 안드로이드에서 집중 모드를 위한 앱 차단 기능을 구현한 것이네요. 서비스와 메소드 채널 통신을 위한 네이티브 안드로이드 코드와 플러터 프론트엔드가 잘 구조화되어 있습니다. 리뷰 결과, 성능, 보안, 코드 유지보수성 측면에서 몇 가지 개선점을 발견했습니다. 구체적으로 SharedPreferences 인스턴스 캐싱, 불필요한 권한 제거, 하드코딩된 값과 문자열 중복 방지, 일부 Dart 코드 정리 등을 제안했습니다. 전반적으로 이 복잡한 기능을 잘 구현하셨습니다.

android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

android:canRetrieveWindowContent="true" 권한은 서비스가 모든 창의 콘텐츠를 읽을 수 있도록 허용하는 매우 강력하고 민감한 권한입니다. 현재 FocusAccessibilityService 구현은 이 권한이 필요 없는 event.packageName만 사용합니다. 최소 권한 원칙에 따라 이 속성을 제거해야 합니다.

val packageName = event.packageName?.toString() ?: return

// Check if blocking is enabled from SharedPrefs
val prefs: SharedPreferences = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

getSharedPreferences가 모든 접근성 이벤트마다 호출되고 있습니다. 접근성 이벤트는 매우 빈번하게 발생할 수 있으므로 비효율적입니다. SharedPreferences 인스턴스를 한 번만 가져와서 (예: onServiceConnected()에서) 멤버 변수에 저장하고 재사용하는 것이 좋습니다.

Log.d(TAG, "Blocking package: $packageName")

// Action: Bring our app to front to make it clear why
val intent = packageManager.getLaunchIntentForPackage("com.company.bookstar")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

패키지 이름 "com.company.bookstar"가 하드코딩되어 있습니다. applicationContext.packageName을 사용하면 패키지 이름이 변경될 경우 발생할 수 있는 문제를 피하고 코드를 더 견고하게 만들 수 있습니다.

Suggested change
val intent = packageManager.getLaunchIntentForPackage("com.company.bookstar")
val intent = packageManager.getLaunchIntentForPackage(applicationContext.packageName)


class MainActivity : FlutterActivity() {
// Channel name as requested
private val CHANNEL = "com.example.blocker/control"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

채널 이름에 com.example이 사용되었습니다. 채널 이름의 접두사로 앱의 패키지 이름을 사용하는 것이 고유성을 보장하는 좋은 방법입니다. 이렇게 하면 다른 플러그인이나 앱과의 잠재적인 충돌을 피할 수 있습니다. lib/core/native_block_controller.dart 파일에서도 함께 수정해야 합니다.

Suggested change
private val CHANNEL = "com.example.blocker/control"
private val CHANNEL = "com.company.bookstar/blocker/control"

Comment on lines +22 to +23
val prefs = getSharedPreferences("FocusModePrefs", Context.MODE_PRIVATE)
prefs.edit().putBoolean("is_active", true).apply()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

SharedPreferences 이름 "FocusModePrefs"와 키 "is_active"가 하드코딩되어 있습니다. 이 값들은 FocusAccessibilityService에서도 사용됩니다. 중복과 잠재적인 불일치를 피하기 위해, 공유 위치(예: FocusAccessibilityService.companion object)에 상수로 정의하고 여기와 서비스에서 참조하는 것이 좋습니다. 이 내용은 27-28라인에도 동일하게 적용됩니다.

import 'package:flutter/services.dart';

class NativeBlockController {
static const MethodChannel _channel = MethodChannel('com.example.blocker/control');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

이 채널 이름은 MainActivity.kt에 정의된 이름과 일치해야 합니다. 채널 이름의 접두사로 앱의 패키지 이름을 사용하는 것이 고유성을 보장하는 좋은 방법입니다.

Suggested change
static const MethodChannel _channel = MethodChannel('com.example.blocker/control');
static const MethodChannel _channel = MethodChannel('com.company.bookstar/blocker/control');

Future<void> startBlocking() async {
try {
await _channel.invokeMethod('startBlock');
print('Blocking started on native side.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

print() 호출은 프로덕션 코드에서 피해야 합니다. package:flutter/foundation.dartdebugPrint (릴리즈 빌드에서는 아무 작업도 하지 않음)를 사용하거나 전용 로깅 패키지를 사용하는 것을 고려해 보세요. 이 파일의 모든 print 호출에 해당됩니다.

Suggested change
print('Blocking started on native side.');
debugPrint('Blocking started on native side.');

Comment on lines +243 to 266
onTap: () async {
if (!_isPermissionGranted) {
// Show dialog to guide user before requesting permission
final bool shouldGoToSettings = await _showPermissionGuideDialog();

if (shouldGoToSettings) {
final bool settingsOpened = await _blockController.requestPermission();
if (settingsOpened) {
// Wait for user to possibly enable it and come back
// Note: We can't know exactly when they come back, but lifecycle listener handles re-check
}
}
// Don't proceed to toggle lock if permission wasn't granted yet
return;
}

// If permission is granted, proceed with blocking/unblocking logic
if (!widget.isLock) { // If currently unlocked (false), tapping means locking (true)
await _blockController.startBlocking();
} else { // If currently locked (true), tapping means unlocking (false)
await _blockController.stopBlocking();
}
widget.onLockToggle(); // Call original toggle callback to update widget.isLock
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

새로운 onTap 로직으로 인해 _buildDeepTimeLock 메소드의 toggleIsLock 파라미터가 더 이상 필요 없게 되었습니다. 이제 widget.onLockToggle()을 직접 호출하고 있기 때문입니다. 코드 명확성을 높이고 사용하지 않는 코드를 제거하기 위해 _buildDeepTimeLock에서 toggleIsLock 파라미터를 제거하고 호출 부분(139라인)도 업데이트하는 것이 좋습니다.

@thdudgus thdudgus linked an issue Jan 28, 2026 that may be closed by this pull request
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

타이머 실행 시 타 앱 잠금

2 participants