본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[android] Kiosk mode app 을 만들자!

by 돼지왕 왕돼지 2017. 8. 7.
반응형

 [android] Kiosk mode app 을 만들자!



http://www.andreas-schrade.de/2015/02/16/android-tutorial-how-to-create-a-kiosk-mode-in-android/

acquire, ACQUIRE_CAUSES_WAKEUP, ACTION_CLOSE_SYSTEM_DIALOGS, ACTION_SCREEN_OFF, activitymanager, addFlags, Android, back button, back button disable, boot complete broadcast, BootReceiver, BOOT_COMPLETED, broadcast, broadcastReceiver, Collapse, collapsepanels, dispatchKeyEvent, FLAG_DISMISS_KEYGUARD, Foreground, FULL_WAKE_LOCK, getMethod, getRunningTasks, GET_TASKS, home, home button, Invoke, isHeld, keepScreenOn, KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP, kiosk mode, LayoutParams, permission, power button, powermanager, receiver, RECEIVE_BOOT_COMPLETED, recent app button, Reflection, registerKioskModeScreenOffReceiver, release, SDK_INT, sendbroadcast, single app, StatusBarManager, system dialog, topActivity, uses-permission, volume button, wake lock, wakelock, WAKE_LOCK, WindowManager, [android] Kiosk mode app 을 만들자!


-

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 를 살펴보자.




반응형

댓글