Tag Archives: Android

안드로이드 키오스크(Kiosk) 앱 개발 (3/3)

앞의 과정(안드로이드 키오스크 앱 개발 1/3, 안드로이드 키오스크 앱 개발 2/3)에서 안드로이드 앱을 기기의 키오스크 모드로 동작시키기 위한 프로그래밍 방법을 살펴봤다. 여기에서는 앞의 과정으로 만든 앱을 실 기기에 배포하는 방법을 살펴본다. 앱을 프로비저닝해서 실 기기에 배포하는 방법(버전별 지원 방법 확인)으로 NFC와 QR 코드로 앱을 설치하는 두 가지 방법이 널리 쓰이고, 여기에서 QR 코드를 사용하는 방법을 살펴보자.

전체 과정은 앱을 빌드하고, 빌드된 APK를 웹 서버에 업로드하고, 이 정보를 바탕으로 QR 코드를 생성하고, 마지막으로 QR코드를 사용해서 프로비저닝된 APK를 실제 기기에 설치하면 된다. 아래에서 개별 과정을 살펴보자.

1. 앱 빌드

릴리즈 버전의 앱은 아래의 2가지 방법으로 빌드하면 된다.

1.1 안드로이드 스튜디오의 Build > Generate Signed Bundle/APK 메뉴를 선택해서 빌드한다.

1.2 터미널(Terminal) 탭에서 아래의 명령으로 APK를 빌드한다.

./gradlew aR 

2. 웹 서버 및 ngrok 툴 설정

이제 빌드한 앱을 다운로드할 수 있는 엔드 포인트를 만들어보자.

2.1 웹 서버

아래 주소에서 간단한 바로 실행할 수 있는 자바 웹 서버를 다운로드 한다.
http://www.jibble.org/miniwebserver/

그리고, 아래의 명령으로 자바 웹 서버를 실행한다.

java -jar SimpleWebServer.jar

2.2 ngrok 툴 설정

아래 주소에서 ngrok 툴을 다운로드 받아서 설치한다.
https://ngrok.com/

내/외부에서 쉽게 도메인을 치고 들어올 수 있게 지원하는 툴이다. 이 툴의 설명을 보면, “secure introspectable tunnels to localhost”로 안전하게 로컬호스트로 터널링을 지원하는 도구라는 것을 알 수 있다.

아래이 명령으로 외부에서 http(s)://xxxxx.ngrok.io 도메인으로 접속할 수 있게 한다.

ngrok http 80 


– 웹 서버와 ngrok 툴 설정화면


– 웹 브라우저로 ngrok 툴이 설정한 주소로 접근한 화면

3. QR 코드 생성

이제 앱을 프로비저닝하는 QR 코드를 만들어 보자. APK 프로비저닝하기 위해서 QR 코드에 입력해야 하는 값은 아래와 같은 같다.

{
"android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME":"net.sjava.examples.kiosk/.KioskDeviceAdminReceiver",
"android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION":"https://745b0d80.ngrok.io/app-release.apk",
"android.app.extra.PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM":"xxxx-xxxx-xxxxxx-xxxxxxxx_xxxx",
"android.app.extra.PROVISIONING_SKIP_ENCRYPTION":true,
"android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED":true,
}

– QR 코드 입력값

위 코드에서 PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME 값은 패키지와 관리자 리시버 클래스 이름이다. PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION은 APK 파일을 다운로드 받을 주소이다. PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM은 APK의 CHECKSUM 값으로 아래의 명령을 사용해서 개발한 앱의 CHECKSUM 값을 구할 수 있다.

apksigner verify -print-certs app-release.apk | grep -Po "(?<=SHA-256 digest:) .*" | xxd -r -p | openssl base64 | tr -d '=' | tr -- '+/=' '-_'

PROVISIONING_SKIP_ENCRYPTION나 PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED은 이름에서 알 수 있듯이 암호화와 기존 시스템앱을 모두 사용할 수 있게 설정한다는 것이다.

위의 값으로 http://down-box.appspot.com/ 에서 위의 과정으로 만들어진 값을 사용해 아래와 같이 QR 코드를 쉽게 만들 수 있다.
위에서 샘플로 만들어본 QR 코드는 http://down-box.appspot.com/qr/GH1ayZg9 주소에서 확인할 수 있지만, 실제 기기에서는 동작하지 않는다.

4. QR 코드로 APK를 관리자 앱으로 설치

테스트할 수 있는 안드로이드 기기를 공장 초기화(Factory Reset)한다. 그리고, 첫 부팅 화면에서 시작 버튼이나 다른 버튼을 사용해서 안드로이드 기기를 시작하지 말고, 바탕 화면을 연속해서 6~7번 탭을 하면 QR 코드를 읽을 수 있는 앱을 실행하게 된다. 또는 안드로이드 7을 사용중인 경우에는 Wifi에 연결한 뒤에 QR 코드를 읽는 앱을 다운로드 해서 실행하는 경우가 있기에 조금 기다리면 QR 코드를 읽는 앱을 볼 수 있다.

이 앱을 사용해서 프로비저닝된 앱을 설치하면, 이 앱은 소유자 권한으로 설치가 되고 이 앱을 통해서 비교적 안전하게 키오스크 모드로 동작하는 앱을 배포할 수 있다.

Reference

https://developers.google.com/android/management/provision-device
https://github.com/googlesamples/android-testdpc

안드로이드 키오스크(Kiosk) 앱 개발 (2/3)

안드로이드 키오스크 앱 개발 (1/3)에서 안드로이드 앱을 키오스크 모드로 동작시키는 방법을 살펴봤다. 여기에서는 앞에서 살펴봤던 예제 프로젝트 앱에 관리자(Admin) 권한을 부여하는 방법과 소유자(Owner) 권한을 부여해서 삭제할 수 없고(예외로 공장 초기화로 삭제할 수 있음), 안드로이드 기기의 기능을 제한하는 방법을 살펴보자.

1. 디바이스 관리자 앱(Device Admin Apps)으로 등록

안드로이드 앱에서 기기를 관리(엔터프라이즈용 기능)할 수 있는 권한을 앱에 부여할 수 있고, 이 권한을 얻으면, DevicePolicyManager(https://developer.android.com/reference/android/app/admin/DevicePolicyManager) API를 사용해서 기기의 기능을 제한할 수 있다. 테스트 하는 기기에 등록된 앱을 살펴보면 아래와 같다.


– 디바이스 관리자 앱들

1.1 DeviceAdminReceiver 추가

앱을 기기 관리자 앱으로 등록하거나 삭제되는 경우에 시그널을 받을 수 있는 브로드캐스트 리시버를 추가한다.

public class KioskDeviceAdminReceiver extends DeviceAdminReceiver {
  static final String TAG = "SIMPLE_KIOSK";

  @Override
  public void onReceive(Context context, Intent intent) {
    Log.i(TAG, "onReceive: " + intent.getAction());
  }

  @Override
  public void onEnabled(Context context, Intent intent) {
    Toast.makeText(context, "Device admin enabled", Toast.LENGTH_SHORT).show();
  }

  @Override
  public void onDisabled(Context context, Intent intent) {
    Toast.makeText(context, "Device admin disabled", Toast.LENGTH_SHORT).show();
  }

  @Override
  public void onLockTaskModeEntering(Context context, Intent intent, String pkg) {
    Log.i(TAG, "onLockTaskModeEntering: " + pkg);
  }
}

– KioskDeviceAdminReceiver.java 파일

이 파일은 앱이 기기 관리자로 등록/해제 등의 이벤트를 받는 관리자 브로드캐스트 리시버이다. 그리고, 관리자 기능과 관련된 시그널을 받기 위해서 아래와 같이 위의 리시버를 AndroidManifest.xml의 의 하위 요소로 추가한다.

<receiver
    android:name=".KioskDeviceAdminReceiver"
    android:description="@string/app_name"
    android:label="@string/app_name"
    android:permission="android.permission.BIND_DEVICE_ADMIN">
    <meta-data
        android:name="android.app.device_admin"
        android:resource="@xml/device_admin_receiver"
        tools:ignore="DeviceAdmin" />
    <intent-filter>
        <action android:name="android.intent.action.DEVICE_ADMIN_ENABLED"/>
    </intent-filter>
</receiver>        

– AndroidManifest.xml에 KioskDeviceAdminReceiver 등록

KioskDeviceAdminReceiver도 다른 브로드캐스트 리시버와 동일하게 AndroidManifest.xml에 등록을 한다.

1.2 device_admin_receiver.xml 추가

이 파일을 /src/main/res/xml/ 폴더에 추가한다. 이 파일에는 앱이 관리자 권한을 얻게 되면, 사용할 정책을 정의한다. 개별 정책에 대한 내용은 DeviceAdminInfo 클래스 API에 정의되어 있다.

 <?xml version="1.0" encoding="utf-8"?>
<device-admin>
    <uses-policies>
        <limit-password />
        <watch-login />
        <reset-password />
        <force-lock />
        <wipe-data />
        <expire-password />
        <encrypted-storage />
        <disable-camera />
    </uses-policies>
</device-admin>   

– 데모 프로젝트의 device_admin_receiver.xml 파일

위 정책을 몇 개만 살펴보면, wipe-data는 로컬 스토리지를 포맷할 수 있는 권한을 가지게 되고, encrypted-storage는 스토리지를 암호화할 수 있고, disable-camera는 카메라 기능을 비활성화할 수 있다.

1.3 기기 관리자 권한 추가

기기에서 앱이 관리자 권한을 가지기 위해서는 일반 앱의 퍼미션 요청과 동일한 형태로 관리자 권한을 요청해야 한다.

  
  private void enableAdmin() {
    if (isDeviceAdminApp()) {
      return;
    }
    Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
    intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, mAdminComponentName);
    // Start the add device admin activity
    startActivityForResult(intent, DEVICE_ADMIN_ADD_REQUEST);
  }

– MainActivity의 기기 관리자 권한 요청 코드

위 코드로 기기 관리자 권한을 요청할 수 있다.


– 기기 관리자 앱 활성화 화면

위 화면은 앱이 가지는 관리자 정책과 활성화를 요청하는 화면이고, 요청을 수락하면 앱은 관리자 권한을 가지고 동작한다.

  
// 일반 앱의 경우에는 isAdminApp, isOwnerApp의 값이 false이다.
2020-05-23 12:17:06.265 4712-4712/net.sjava.examples.kiosk I/SIMPLE_KIOSK: isAdminApp: false
2020-05-23 12:17:06.266 4712-4712/net.sjava.examples.kiosk I/SIMPLE_KIOSK: isOwnerApp: false

// 관리자 권한을 가지게 되면 isAdminApp의 값이 true인 것을 볼 수 있다.
2020-05-23 12:19:42.083 4976-4976/net.sjava.examples.kiosk I/SIMPLE_KIOSK: isAdminApp: true
2020-05-23 12:19:42.084 4976-4976/net.sjava.examples.kiosk I/SIMPLE_KIOSK: isOwnerApp: false

2. 앱에 소유자 앱 권한 부여

이제 앱에 소유자 권한을 부여하는 방법을 살펴보자. 여기에서는 소유자 앱의 권한을 주기 위해서 adb shell 명령을 사용할 것이다. 소유자 앱으로 테스트 하다가 문제가 생겨서 기기를 사용할 수 없게 되거나 공장 초기화(Factory Reset)를 해야 하는 경우가 발생할 수 있다. 그래서 아래와 같이 Applicaion 요소에 android:testOnly=”true” 를 추가해서 기기의 잠금으로 인한 문제를 방지한다.

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:testOnly="true"
    android:theme="@style/AppTheme">

– AndroidManifest.xml의 application 요소에 android:testOnly=”true”를 추가한 예

아래는 앱에 소유자 권한을 부여하고, 관리자 앱을 비활성화하는 방법이다.

  
# 소유자 앱 활성화
adb shell dpm set-device-owner net.sjava.examples.kiosk/.KioskDeviceAdminReceiver
# 관리자 앱 비활성화
adb shell dpm remove-active-admin net.sjava.examples.kiosk/.KioskDeviceAdminReceiver

// 소유자 앱 활성화 뒤
2020-05-23 12:34:14.450 4976-4976/net.sjava.examples.kiosk I/SIMPLE_KIOSK: isAdminApp: true
2020-05-23 12:34:14.451 4976-4976/net.sjava.examples.kiosk I/SIMPLE_KIOSK: isOwnerApp: true

// 관리자 앱 비활성화 뒤
2020-05-23 12:36:26.200 4976-4976/net.sjava.examples.kiosk I/SIMPLE_KIOSK: isAdminApp: false
2020-05-23 12:36:26.201 4976-4976/net.sjava.examples.kiosk I/SIMPLE_KIOSK: isOwnerApp: false

만약, adb shell dpm set-device-owner 명령에 아래와 같은 에러를 보게 된다면, 설정(Settings) > 계정(Account)에 등록된 계정을 삭제하고 다시 실행하면 된다.

  
adb shell dpm set-device-owner net.sjava.examples.kiosk/.KioskDeviceAdminReceiver

java.lang.IllegalStateException: Not allowed to set the device owner because there are already some accounts on the device
at android.os.Parcel.createException(Parcel.java:2079)
at android.os.Parcel.readException(Parcel.java:2039)
at android.os.Parcel.readException(Parcel.java:1987)
at android.app.admin.IDevicePolicyManager$Stub$Proxy.setDeviceOwner(IDevicePolicyManager.java:8392)
at com.android.commands.dpm.Dpm.runSetDeviceOwner(Dpm.java:203)
at com.android.commands.dpm.Dpm.onRun(Dpm.java:115)
at com.android.internal.os.BaseCommand.run(BaseCommand.java:56)
at com.android.commands.dpm.Dpm.main(Dpm.java:41)
at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:338)
Caused by: android.os.RemoteException: Remote stack trace:
at com.android.server.devicepolicy.DevicePolicyManagerService.enforceCanSetDeviceOwnerLocked(DevicePolicyManagerService.java:8647)
at com.android.server.devicepolicy.DevicePolicyManagerService.setDeviceOwner(DevicePolicyManagerService.java:7809)
at android.app.admin.IDevicePolicyManager$Stub.onTransact(IDevicePolicyManager.java:3270)
at android.os.Binder.execTransactInternal(Binder.java:1021)
at android.os.Binder.execTransact(Binder.java:994)

– adb shell dpm set-device-owner 에러 로그

아래 화면은 터미널에서 앱에 소유자 권한을 추가하고, 어드민 권한을 제거한 결과를 보여주는 화면이다.

– 소유자 권한 추가 및 해제 화면

3. 기능 제한

여기에서는 앱이 소유자 권한을 가지고 안드로이드 기기에서 기능을 제한하는 방법에 대해서 살펴보자. 간단하게 카메라 기능, 사용자의 일부 기능 제한, 그리고 앱 사용을 제한하는 방법에 대해서 살펴보자.

3.1 카메라 기능 제한

아래 코드로 기기 카메라 사용을 제한할 수 있다.

 
private void disableCamera(boolean disabled) {
  mDevicePolicyManager.setCameraDisabled(mAdminComponentName, disabled);
}

– 카메라 기능 제한

이 코드로 기기의 카메라 기능을 제한할 수 있다. 그리고, 사용자의 기능 제한은 3.2에서 확인할 수 있고, 많은 기능을 제한할 수 있는 것을 알 수 있다.

3.2 사용자 기능 제한

아래 코드로 사용자의 기능을 제한할 수 있다.

 
static final ArrayList mUserRestrictions = new ArrayList<>(
    Arrays.asList(
        UserManager.DISALLOW_SMS,
        UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
        //UserManager.DISALLOW_USB_FILE_TRANSFER,
        UserManager.DISALLOW_BLUETOOTH
    )
);

private void setUserRestrictions(boolean restricted) {
    for (String restriction : mUserRestrictions) {
        if (restricted) {
            mDevicePolicyManager.addUserRestriction(mAdminComponentName, restriction);
        } else {
            mDevicePolicyManager.clearUserRestriction(mAdminComponentName, restriction);
        }
    }
}

– 사용자 기능 제한

위 mUserRestrictions은 제한될 사용자의 기능을 정의했다. 코드에서 알 수 있듯이, UserManager에 제한할 기능이 정의되어 있고 필요한 기능은 확인해보면서 추가하면 된다.

3.3 앱 사용 제한

아래 코드로 설치된 앱 사용을 제한할 수 있다.

 
static final String[] mSuspendedPackageNames = {"com.twitter.android",
    "com.facebook.katana",
    "com.google.android.apps.nbu.files"
};

private void setPackagesSuspended(String[] packageNames, boolean suspended) {
    if(Build.VERSION.SDK_INT >= 24) {
        mDevicePolicyManager.setPackagesSuspended(mAdminComponentName, packageNames, suspended);
    }
}

– 앱 사용 제한

위 코드에서 mSuspendedPackageNames는 사용을 제한할 앱 목록을 가지고 있고, DevicePolicyManager의 setPackagesSuspended() 메서드를 호출해서 앱 사용을 제한할 수 있다. 더불어 DevicePolicyManager의 setApplicationHidden() 메서드로 앱 자체를 숨겨버릴 수도 있다.

이 프로젝트의 소스는 https://github.com/mcsong/SimpleKioskDemo에서 확인할 수 있다.


– 소유자 권한을 가진 앱으로 몇 개의 앱과 카메라 기능을 비활성화한 예제

위에서 살펴본 기능 외에도 많은 기능을 제한할 수 있다. 소유자 권한으로 안드로이드 API가 제공하는 기능 제한은 DevicePolicyManager 클래스에서 확인할 수 있다.

지금까지 안드로이드 앱을 키오스크 모드로 동작하고, 소유자 권한을 획득해서 안드로이드 기기를 온전히 키오스크 기기나 특정 용도에 맞게 사용할 수 있는 방법을 살펴봤다. 안드로이드 기기를 관리하는 방법으로 제조사에서 제공하는 MDM 솔루션(삼성 Knox나 LG Gate)을 사용할 수 있는데, MDM 솔루션을 사용하면 위의 기능에 더불어 제조사에서 자신들의 기기만을 위해서 추가한 기능도 관리할 수 있어서 조금 더 스트릭하게 관리할 수 있다. 개인적으로 몇 개의 MDM 솔루션으로 작업을 해 본 결과, 지금까지는 삼성 Knox가 가장 편리하고 기능적으로도 우수하다.

Reference

https://developer.android.com/guide/topics/admin/device-admin
https://developer.android.com/work/dpc/dedicated-devices/lock-task-mode

안드로이드 키오스크(Kiosk) 앱 개발 (1/3)

이제 우리는 일상에서 많은 키오스크(Kiosk) 기기를 사용한다. 맥XXX 햄버거 체인점에서 주문하는 기기, 공항에서 층별 정보나 항공사 위치를 확인할 수 있는 기기, 회사 로비에 인포 데스크 없이 방문자를 확인할 수 있는 기기 등이 우리가 주변에서 쉽게 사용하고 있는 키오스크 기기이다. 이 기기 또한 우리가 널리 사용하고 있는 윈도, 리눅스, 안드로이드, IOS 등을 사용한다. 먼저 키오스크의 정의를 살펴보면 아래와 같다.

인터랙티브 키오스크(interactive kiosk)는 특수한 하드웨어와 소프트웨어를 갖춘 단말기의 하나로, 커뮤니케이션, 상업, 엔터테인먼트, 교육을 위한 정보 및 애플리케이션에 대한 접근 권한을 제공한다.

안드로이드 또한 키오스크 기능을 개발할 수 있게 API를 제공하기에, 키오스크 앱을 개발하는 방법과 앱이 키오스크 앱으로 동작하는데 필요한 고려사항에 대해서 세 파트(1/3, 2/3, 3/3)로 나눠서 살펴본다.

우선 키오스크 앱을 개발하기 전에 앱이 획득할 수 있는 권한에 대해서 살펴보자. 안드로이드 앱은 크게 시스템 앱과 일반 앱으로 구분할 수 있고, 안드로이드 제조사나 제조사 파트너의 경우에만 자신의 앱을 시스템 앱으로 배포할 수 있기에, 여기서는 일반 앱을 대상으로 한다. 안드로이드 앱의 권한은 일반 앱 -> 관리자(Admin) 앱 -> 소유자(Owner) 앱의 순으로 안드로이드 기기를 제어할 수 있는 권한을 가지게 된다.

일반 앱: 일반적인 방법으로 안드로이드 기기에 설치한 앱이다.
관리자(Admin) 앱: 일반 앱에서 사용자의 권한 요청과 수락의 과정으로 관리자(Admin) 권한을 가지는 앱이 될 수 있다. 관리자 권한은 다음에 살펴보겠지만, XML 정책 파일에 필요한 권한을 명시해서 권한을 요청한다. 이 과정은 일반 앱의 퍼미션 요청 및 수락 과정과 비슷하다.
소유자(Owner) 앱: 소유자 앱은 삭제할 수 없고, 관리자 권한을 포함해서 안드로이드 기기의 기능 및 앱을 제한할 수 있는 막강한 권한을 가진다.

일반 앱에서 관리자 권한을 요청해서 관리자 앱이 될 수 있지만, 소유자 앱의 경우에는 프로그래밍으로 권한을 얻을 수가 없다. 일반 앱이 소유자 앱이 되는 방법으로 2가지가 있는데 첫 번째가 제조사에서 제공하는 MDM 솔루션을 사용하는 방법이다. 이 경우에는 제조사가 MDM 솔루션을 제공해야 하고, 제공하는 MDM 솔루션을 무료 또는 구매하면 된다. 두 번째 방법은 APK를 프로비저닝(Provisioning)해서 QR 코드나 NFC를 사용해서 앱에 설치하는 방법이 있다. 여기에서 살펴보는 방법은 APK의 QR 코드를 만들고 앱에 소유자 앱으로 설치하는 방법을 살펴볼 것이다. 그리고, APK를 프로비저닝하는 방법은 https://developers.google.com/android/management/provision-device#provisioning_methods 에서 확인할 수 있다.

먼저 일반 앱으로 키오스크 모드 앱을 개발해 보자.

1. 키오스크 모드 시작 및 중지

키오스크 모드는 Activity의 startLockTask() 메서드를 호출해서 시작하고, 중지는 stopLockTask() 메서드를 호출하면 된다.

mStartLockButton.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    LockUtil.lock(MainActivity.this);
    startLockTask();
  }
});

mEndLockButton.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    LockUtil.unLock(MainActivity.this);
    stopLockTask();
  }
});

– MainActivity의 키오스크 시작 및 중지 코드

위 코드는 2개의 버튼으로 키오스크 모드를 시작하고 중지하는 것을 볼 수 있고, LockUtil 클래스는 키오스크 모드를 시작했다는 정보를 유지한다. 이 정보를 유지해서 안드로이드 기기가 재시작하는 경우에 바로 MainActivity를 실행시켜서 키오스크 모드를 시작할 수 있게 한다.


– 키오스크 시작화면

일반 앱으로 키오스크 모드를 실행하면, 위 화면과 같은 다이얼로그를 보게 되고 확인을 클릭해야 키오스크 모드로 동작한다. 키오스크 모드로 동작시키기 위해서 앱을 실행해서 매번 확인을 클릭해야 한다면 유지 및 관리 이슈가 클 것이다. 이 이슈는 앱을 소유자 권한으로 실행하고 키오스크 모드를 시작하면 이 이슈를 해결 할 수 있다.

2. 부팅 완료(BOOT_COMPLETED) 브로드 캐스트 추가

브로드 캐스트는 안드로이드 기기가 부팅을 완료한 후에 바로 키오스크 모드로 잠겨야 할 필요가 있는 경우에 사용할 수 있다.


    
        
    

– AndroidManifest.xml에 BootCompleteReceiver 추가

위 코드는 BOOT_COMPLETED 브로드 캐스트를 받아서 처리하는 클래스(BootCompleteReceiver)와 속성에 대해서 정의하고 있다. android:directBootAware=”true”의 경우에는 사용자가 잠금을 해제하지 않고도 실행할 수 있게 한다.

다음으로 BootCompleteReceiver를 살펴보자.

public class BootCompleteReceiver extends BroadcastReceiver {
  static final String TAG = "SIMPLE_KIOSK";

  @Override
  public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    Log.i(TAG, "onReceive: " + action);
    if(Intent.ACTION_BOOT_COMPLETED.equals(action)) {
        startActivity(context);
    }
  }

  static void startActivity(Context context) {
    Intent i = new Intent(context, MainActivity.class);
    // For Android 9 and below
    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
      i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
      context.startActivity(i);
      return;
    }
    // Android 10 ?
  }
}

- BootCompleteReceiver 코드

위 코드는 부팅을 완료하면, 안드로이드 OS가 ACTION_BOOT_COMPLETED 액션과 함께 BootCompleteReceiver를 호출한다. 이 액션이 들어온 경우에 MainActivity를 실행하는 것을 알 수 있다. 안드로이드 10의 경우에 백그라운드로 동작하는 컴포넌트에서는 액티비티를 실행할 수 없기에, 현재는 안드로이드 9 및 이하의 버전에서만 안드로이드 시작 후에 MainActivity를 실행해서 바로 키오스크 모드로 동작하게 할 수 있다. 안드로이드 10 버전은 다음에 살펴보도록 하겠다.

아래 영상은 안드로이드 8.1.0 에뮬레이터에서 일반 앱으로 키오스크 모드가 어떻게 동작하는 지를 확인할 수 있고, 안드로이드를 재시작한 뒤에 앱이 자동으로 키오스크 모드로 진입하는 것도 확인할 수 있다.


이 프로젝트의 소스는 https://github.com/mcsong/SimpleKioskDemo에서 확인할 수 있다.

Reference

- https://ko.wikipedia.org/wiki/인터랙티브_키오스크
- https://developer.android.com/guide/topics/admin/device-admin
- https://developer.android.com/work/dpc/dedicated-devices/lock-task-mode

안드로이드 테마 기능 적용하기

안드로이드 10부터 시스템 수준에서 어두운 테마(Dark theme)를 지원하기 시작했고, 이제 안드로이드 앱들은 테마(밝은/어두운/시스템) 기능을 필수적으로 지원해야 할 때이다. 어두운 테마로 변경하면, 앱에서 사용하는 컴포넌트의 색을 어두운 계열로 변화 시켜 시인성을 높이고, 사용자 눈의 피로도를 줄이고, 배터리의 사용을 절약시키는 이점도 부가적으로 얻을 수 있다.

  
– 안드로이드 10 에뮬레이터의 설정 화면에서 어두운 테마 켜기

안드로이드 10에서 사용자가 설정 화면에서 어두운 테마를 선택했다면, 설치된 앱들도 어두운 색상으로 동작하길 원할 것이기에 사용자에게 일관적인 UI/UX를 경험할 수 있게 하기에 테마 기능 적용은 중요한 UX 요구사항이라고 볼 수 있게 되었다. 테마 변경을 살펴보기 전에 안드로이드 화면의 색상값에 대해서 한번 살펴보자.


– 안드로이드 화면에서의 테마 색상 요소 값

위 테마 색상은 스타일 파일(/res/styles.xml)에서 아래와 같이 확인할 수 있다.

앱에서 AppCompat 테마를 사용하는 경우에는, parent=”Theme.AppCompat.DayNight”로 선언을 해야 하고, MaterialComponents 테마를 사용하는 경우, parent=”Theme.MaterialComponents.DayNight”로 선언해야 한다.

이제 테마 기능을 추가해 보자. 테마 기능을 추가하는데 필요한 구성은 다음과 같다:

– 테마별 색상 변경을 위한 폴더 구조
– 스타일 파일(styles.xml)의 변경 요소들
– 테마 변경을 위한 코드 작성
– 테마 변경과 관련된 이슈들

1. 테마별 색상 변경을 위한 폴더 구조

1.1 테마 파일과 색상 파일

안드로이드 앱에서 기본값으로 읽는 테마와 색상 정보는 /res/values/ 폴더에 스타일 파일(styles.xml)과 색상 파일(colors.xml)에 선언한다. 어두운 테마가 읽는 테마 정보와 색상 정보는 /res/values-night/ 폴더에 같은 파일 이름으로 선언한다. 많은 경우에는 /res/values-night/ 폴더에 색상 파일만 위치시켜도 테마 기능이 잘 동작한다.

1.2 이미지 파일

만약 밝은 테마와 어두운 테마별로 보이는 이미지가 다른 경우, 밝은 테마에 보이는 이미지는 drawable-..hdpi 폴더에 어두운 테마에 보이는 이미지는 darwable-night-…hdpi 폴더에 넣어준다. mipmap을 사용하는 경우에도 drawable과 같은 패턴으로 mipmap-night-… 폴더를 사용하면 된다.

테마 변경 결과 화면을 먼저 살펴보자.

   

이 화면과 예제 소스는 https://github.com/mcsong/ThemeDemo 에서 확인할 수 있다.

2. 스타일 파일에서의 변경 요소들

스타일 파일(styles.xml)에서의 요소들에 대해서 살펴보자. 안드로이드 테마를 변경하는 경우에 색상 파일(colors.xml)만 변경해도 잘 동작할 것이다. 하지만, 시작 화면(Splash Screen) 테마의 경우에는 안드로이드 10 이후부터 어두운 테마에 어울리는 스타일을 읽어 들여야 하므로 /res/values-night/ 의 스타일 파일에 시작 화면 테마도 선언해야 한다.

시작 화면 테마는 앱이 화면(MainActivity가 주로 로딩)을 보이기 전에 보이는 화면의 구성이라고 보면 된다. 안드로이드 앱을 실행하면 런타임이 앱을 읽어 들이는 시간에 사용자가 느리다는 것을 인지하게 되고, 이것을 개선하는 방법으로 시작 화면 테마 사용을 권장하고 있다. 간단한 예제는 아래의 링크에서 확인할 수 있다.

시작 화면 테마 구성 방법 : https://android.jlelse.eu/launch-screen-in-android-the-right-way-aca7e8c31f52

예제 프로젝트에서도 시작 화면 테마를 사용하고 있으니, 예제 프로젝트의 소스를 살펴보면 시작 화면 테마를 어떻게 사용하고 있는지 알 수 있다. 이제 스타일 파일을 살펴보자.

    
    




– /res/values/styles.xml 파일

이 파일은 밝은 테마를 구성하는 스타일 파일이고, 밝은 계열의 색으로 배경을 구성한다. 그리고, android:windowLightNavigationBar의 값을 true로 설정해서 안드로이드 기기 아래의 컨트롤 버튼을 잘 보이게 한다.

    
    






– /res/values-night/styles.xml 파일

이 파일은 어두운 테마를 구성하는 스타일 파일이고, 어두운 계열의 색으로 배경을 구성한다. 그리고, android:windowLightNavigationBar의 값을 false로 설정해서 아래 컨트롤 버튼을 잘 보이게 한다.

3. 테마 변경을 위한 코드 작성

테마 변경을 위한 소스 코드는 https://github.com/mcsong/ThemeDemo을 기준으로 살펴보자.

package net.sjava.examples.theme;
import android.app.Application;

public class ThemeApp extends Application {

  @Override
  public void onCreate() {
    super.onCreate();

    ThemeUtil.applyTheme(this);
  }
}

– ThemeApp.java 소스 코드

이 클래스는 어플리케이션 소스를 상속받아서, 앱을 실행하면 가장 먼저 실행해서 예제 앱에서 변경한 테마를 읽게 한다.

package net.sjava.examples.theme;

import android.content.Context;
import android.os.Build;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;

public class ThemeUtil {

  static final String THEME_KEY = "theme_value";

  public static void applyTheme(@NonNull Context context) {
    int option = SharedPrefsUtil.getInt(context, THEME_KEY, 0);
    applyTheme(context, option);
  }

  // 0 : light, 1 : dark, 2 : daytime
  public static void applyTheme(@NonNull Context context, int option) {
    // 테마 값 저장
    SharedPrefsUtil.putInt(context, THEME_KEY, option);

    if (option == 0) {
      AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
      return;
    }

    if (option == 1) {
      AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
      return;
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
      AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
    } else {
      AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
    }
  }
}

– ThemeUtil.java 소스 코드

이 소스는 실제로 테마를 변경하는 소스로, 테마를 변경하면 변경된 값을 저장한다. 시스템에 따라 테마를 활성화시키는 것을 살펴보면, 안드로이드 10에서는 설정에서 어두운 테마를 활성화시킬 수 있고, 안드로이드 9와 이하 버전에서는 배터리가 절전 모드인 경우 또는 현재 시간이 저녁시간대인 경우에 어두운 테마가 활성화 된다. 그래서 안드로이드 10에서는 AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM의 값으로, 안드로이드 10 이하 버전에서는 AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY의 값으로 시스템에 적용된 테마를 앱에 적용할 수 있다.

    findViewById(R.id.theme_light_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        ThemeUtil.applyTheme(v.getContext(), 0);
      }
    });

    findViewById(R.id.theme_dark_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        ThemeUtil.applyTheme(v.getContext(), 1);
      }
    });

    findViewById(R.id.theme_system_default_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        ThemeUtil.applyTheme(v.getContext(), 2);
      }
    });

– 테마를 변경하는 소스

예제 프로젝트의 MainActivith.java 소스를 살펴보면, 위의 코드를 확인할 수 있다. 개별 버튼을 누르면 테마가 변경되는 것을 확인할 수 있다.

4. 테마 변경과 관련된 이슈들

이제 앱에서 테마를 쉽게 변경할 수 있다. 하지만, 이것도 기존의 앱에서 업그레이드하다 보면 문제가 발생 할 수 있고, 테마 기능을 추가하면서 문제가 된 이슈에 대해서 살펴보자.

우선, 코드상에서 안드로이드 10 버전에 추가된 API를 확인하려면, compileSdkVersion 29로 컴파일 SDK 버전을 맞추면 된다. 그리고, 빌드하는 앱의 SDK 버전은 28 또는 29로 맞추면 되겠다. 혹시 29를 사용하면 문제가 있는 경우에는 targetSdkVersion을 28로 설정해서 컴파일해도 된다. 그래서 빌드 파일(build.xml)에 아래와 같이 사용하면 된다.

android {
   compileSdkVersion 29

   defaultConfig {
      applicationId "net.sjava.appstore.demo"
      minSdkVersion 15
      targetSdkVersion 28
      versionCode 1
      versionName "1.0"
   }
......
}

안드로이드 앱에서 테마를 변경하려면 액티비티(Activity)를 다시 생성해야 한다. 그래서, 프레그먼트(Fragment)에서 테마를 변경하려면 테마를 변경하고, 액티비티의 recreate() 메서드를 수동으로 호출해야 했다. 최신 androidx.appcompat:appcompat:1.1.0 를 사용하면, 액티비티의 recreate() 메서드를 호출하지 않아도 액티비티를 다시 생성해서 테마 변경을 바로 확인할 수 있게 한다.

그리고, 아래와 같이 configChange의 uiMode를 선언한 경우에는 시스템에서 어두운 테마로 변경해도 액티비티의 recreate()가 호출되지 않기 때문에 주의해야 한다.


마지막으로 외부 라이브러리를 사용한 경우 테마별 사용한 색이 달라서 이질감이 들 수 있는 경우가 있다. 사용하는 라이브러리에서 안드로이드 기본색을 사용하지 않고 자신의 색을 사용하는 경우에 이런 문제가 발생할 수 있다. 이 경우 라이브러리 소스를 가져와서 소스를 수정해서 사용하는 방법으로 해결하면 된다.

+ 예제 프로젝트 설명

+ reference

– 테마 예제: https://github.com/mcsong/ThemeDemo
– 앱 시작 시간: https://developer.android.com/topic/performance/vitals/launch-time#java
– 머티리얼 디자인 적용: https://developer.android.com/guide/topics/ui/look-and-feel?hl=ko
– 스타일 및 테마: https://developer.android.com/guide/topics/ui/look-and-feel/themes?hl=ko
– 어두운 테마: https://developer.android.com/guide/topics/ui/look-and-feel/darktheme?hl=ko

안드로이드 화면에 실행중인 앱 확인하는 방법

안드로이드 5(롤리팝) 이후부터는 안드로이드 기기에 실행중인 앱을 알 수 없게 되었습니다. 그래서, 안드로이드 5 이후 버전에서는 기기에 저장하고 있는 통계 데이터를 기반으로 완전히 정확하지는 않지만, 대략적으로 정확한 정보를 가져올 수 있습니다. UsageStatsManager를 사용해서 현재 안드로이드 기기의 화면에 실행중인 앱을 검색하는 방법을 살펴보자.

아래는 UsageStatsManager를 사용해서 안드로이드 기기에 저장되어 있는 이벤트 데이터를 검색하고, 이벤트가 포그라운드(Event.MOVE_TO_FOREGROUND 또는 Event.ACTIVITY_RESUMED)인 것을 확인해서 마지막 포그라운드 이벤트에 해당하는 패키지 이름을 가져온다. 이 정보는 대체로 정확하고, 아래 코드로 가져온 패키지가 현재 화면에서 실행중인 것을 확인 할 수 있다.

1. 앱의 AndroidManifest.xml에 아래의 권한을 추가


2. 안드로이드 기기에서 실행중인 앱 가져오기

public static String getTopPackageName(@NonNull Context context) {
  UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);

  long lastRunAppTimeStamp = 0L;

  final long INTERVAL = 1000 * 60 * 5;
  final long end = System.currentTimeMillis();
  // 1 minute ago
  final long begin = end - INTERVAL;

  LongSparseArray packageNameMap = new LongSparseArray<>();
  final UsageEvents usageEvents = usageStatsManager.queryEvents(begin, end);
  while (usageEvents.hasNextEvent()) {
    UsageEvents.Event event = new UsageEvents.Event();
    usageEvents.getNextEvent(event);

    if(isForeGroundEvent(event)) {
      packageNameMap.put(event.getTimeStamp(), event.getPackageName());
      if(event.getTimeStamp() > lastRunAppTimeStamp) {
        lastRunAppTimeStamp = event.getTimeStamp();
      }
    }
  }

  return packageNameMap.get(lastRunAppTimeStamp, "");
}

3. 이벤트가 포그라운드 이벤트인지 확인

private static boolean isForeGroundEvent(UsageEvents.Event event) {
  if(event == null) {
    return false;
  }

  if(BuildConfig.VERSION_CODE >= 29) {
    return event.getEventType() == UsageEvents.Event.ACTIVITY_RESUMED;
  }

  return event.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND;
}

위 코드를 사용해서 완전히 정확하지는 않지만, 대체로 정확하게 안드로이드 화면에서 실행중인 앱을 알 수 있다.