[android] Kiosk mode app 을 만들자!
http://www.andreas-schrade.de/2015/02/16/android-tutorial-how-to-create-a-kiosk-mode-in-android/
-
Kiosk mode 가 무엇을 말하는가?
다른 앱은 실행되지 않는 single app 만 실행하는 그런 앱을 이야기한다.
-
고려해야 할 상황들은 다음과 같다.
back button
home button
recent app button
power button
volume button
-
먼저 kiosk 는 부팅과 동시에 해당 앱이 시작되어야 하기 떄문에, boot complete br 을 처리해야 한다.
@Manifest
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<receiver android:name=".BootReceiver">
<intent-filter >
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
@BootReceiver
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent myIntent = new Intent(context, MyKioskModeActivity.class);
myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(myIntent);
}
}
-
Back button 을 disable 시키자.
@Override
public void onBackPressed() {
// nothing to do here
// … really
}
-
Power button 을 disable 시키자.
framework 수정 없이는 원론적으로는 disable 이 불가능하다.
하지만 이를 detect 해서 무언가는 할 수 있다.
짧은 click 에 대해서는 ACTION_SCREEN_OFF br 을 받아서 wake lock 을 잡는 방법으로 이 문제를 해결할 수 있다.
@Manifest
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:name=".AppContext"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
@Application
public class AppContext extends Application {
private AppContext instance;
private PowerManager.WakeLock wakeLock;
private OnScreenOffReceiver onScreenOffReceiver;
@Override
public void onCreate() {
super.onCreate();
instance = this;
registerKioskModeScreenOffReceiver();
}
private void registerKioskModeScreenOffReceiver() {
// register screen off receiver
final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
onScreenOffReceiver = new OnScreenOffReceiver();
registerReceiver(onScreenOffReceiver, filter);
}
public PowerManager.WakeLock getWakeLock() {
if(wakeLock == null) {
// lazy loading: first call, create wakeLock via PowerManager.
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "wakeup");
}
return wakeLock;
}
}
@OnScreenOffReceiver
public class OnScreenOffReceiver extends BroadcastReceiver {
private static final String PREF_KIOSK_MODE = "pref_kiosk_mode";
@Override
public void onReceive(Context context, Intent intent) {
if(Intent.ACTION_SCREEN_OFF.equals(intent.getAction())){
AppContext ctx = (AppContext) context.getApplicationContext();
// is Kiosk Mode active?
if(isKioskModeActive(ctx)) {
wakeUpDevice(ctx);
}
}
}
private void wakeUpDevice(AppContext context) {
PowerManager.WakeLock wakeLock = context.getWakeLock(); // get WakeLock reference via AppContext
if (wakeLock.isHeld()) {
wakeLock.release(); // release old wake lock
}
// create a new wake lock...
wakeLock.acquire();
// ... and release again
wakeLock.release();
}
private boolean isKioskModeActive(final Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
return sp.getBoolean(PREF_KIOSK_MODE, false);
}
}
@ Activity
// setContentView 전에 아래 코드를 넣어 keyguard 를 무시할 수도 있다.
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
-
Long power button press 도 막아보자.
아래와 같이 window 가 focus 를 잃을 때, SYSTEM DIALOG 를 무조건 닫는 broadcast 를 보내는 것이다.
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(!hasFocus) {
// Close every kind of system dialog
Intent closeDialog = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
sendBroadcast(closeDialog);
}
}
-
Volume button 도 막아볼까?
private final List blockedKeys = new ArrayList(Arrays.asList(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP));
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (blockedKeys.contains(event.getKeyCode())) {
return true;
} else {
return super.dispatchKeyEvent(event);
}
}
-
Home button 도 막아보자.
android 4 부터 home button disable 은 어려워졌다.
여기서 사용 가능한 trick 은 다른 앱이 foreground 로 올라오는 것을 detect 해서 내 activity 를 launch 시키는 것이다.
다음과 같은 service 를 통해 이를 가능하게 한다.
@Manifest
<uses-permission android:name="android.permission.GET_TASKS"/>
@KioskService
public class KioskService extends Service {
private static final long INTERVAL = TimeUnit.SECONDS.toMillis(2); // periodic interval to check in seconds -> 2 seconds
private static final String TAG = KioskService.class.getSimpleName();
private static final String PREF_KIOSK_MODE = "pref_kiosk_mode";
private Thread t = null;
private Context ctx = null;
private boolean running = false;
@Override
public void onDestroy() {
Log.i(TAG, "Stopping service 'KioskService'");
running =false;
super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "Starting service 'KioskService'");
running = true;
ctx = this;
// start a thread that periodically checks if your app is in the foreground
t = new Thread(new Runnable() {
@Override
public void run() {
do {
handleKioskMode();
try {
Thread.sleep(INTERVAL);
} catch (InterruptedException e) {
Log.i(TAG, "Thread interrupted: 'KioskService'");
}
}while(running);
stopSelf();
}
});
t.start();
return Service.START_NOT_STICKY;
}
private void handleKioskMode() {
// is Kiosk Mode active?
if(isKioskModeActive()) {
// is App in background?
if(isInBackground()) {
restoreApp(); // restore!
}
}
}
private boolean isInBackground() {
ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> taskInfo = am.getRunningTasks(1);
ComponentName componentInfo = taskInfo.get(0).topActivity;
return (!ctx.getApplicationContext().getPackageName().equals(componentInfo.getPackageName()));
}
private void restoreApp() {
// Restart activity
Intent i = new Intent(ctx, MyActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ctx.startActivity(i);
}
public boolean isKioskModeActive(final Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
return sp.getBoolean(PREF_KIOSK_MODE, false);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
이 문제의 단점은 롤리팝부터는 이 방법을 사용할 수 없다는 것.
-
Screen dim 도 막자.
activity 의 root layout 에 다음 속성을 준다.
android:keepScreenOn="true"
-
Statusbar 를 막는 것도 중요하징.
Class statusBarManager = null;
Object service = getSystemService("statusbar");
try{
statusBarManager=Class.forName("android.app.StatusBarManager");
if(statusBarManager != null){
if (Build.VERSION.SDK_INT <= 16) {
Method collapse = statusBarManager.getMethod("collapse");
collapse.invoke(service);
} else {
Method collapse2 = statusBarManager.getMethod("collapsePanels");
collapse2.invoke(service);
}
}
}catch(Exception ex){
ex.printStackTrace();
}
-
Home button 을 막는 것이 롤리팝부터 안 되기 때문에 kiosk app 자체를 home screen 으로 만들 수 있다.
@Manifest
<category android:name="android.intent.category.HOME"/>
혹은 kiosk 앱의 pause 당시 preference 를 쓰고,
service 에서 이 preference 값을 읽는 등으로 home button 이나 다른 앱 점프 효과를 막을 수 있다.
-
"android:excludeFromRecents=“true”
recent 에서 kiosk process 를 kill 하지 못하게 하는 것도 중요하다.
-
기타 여러가지 추가 제약사항 해결방안은
original link 의 comment 를 살펴보자.
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[android] Notification 에 meta data 를 추가하자 (0) | 2017.08.24 |
---|---|
[Android] 새로운 Dex Compiler 가 나온다네용 (0) | 2017.08.13 |
[android] Play Store 에 대한 미신 (0) | 2017.08.06 |
[android] ListView scroll 할 때 Toolbar 감추기 (0) | 2017.08.05 |
[android] design support library (0) | 2017.08.04 |
댓글