Implementing Kiosk Mode in Android

Standard

What is Kiosk Mode?

What is “Kiosk Mode” and why would you want to use it? Kiosk Mode basically allows a single Android application to have full control of the system. The user is prevented from leaving the current application or running other applications. Think of a hypothetical ATM machine that is running Android. It would be bad if the user could get out of the “ATM app” and start poking around with the underlying system. The idea is to run only one custom app and prevent any other applications to run (or only whitelisted applications). In this blog post I would like to share the approaches that you can take and the possible challenges you’ll face while developing these type of applications.

General Requirements for such applications:

  • Only one app is allowed to run.
  • App should automatically boot on device’s startup.
  • Only whitelisted contacts are allowed to be contacted (via Phone or SMS)
  • Only whitelisted apps are allowed to be used.
  • Tracking which applications are installed or uninstalled.
  • Disable status bar.

Kiosk Mode is implemented by disabling certain Android features that can be used to leave the current application or start a new application. Specifically, we need to disable or change the following features of Android:

  • The back button
  • The home button
  • The recent apps button
  • The power button
  • The volume buttons
  • The “notification drawer” or “status bar”

Unfortunately, Android does not come with a full-fledged Kiosk Mode that we can simply turn on. Instead, we need to disable or change the behavior of each of the four features individually. So, in this article i will try to provide a brief overview of how Kiosk Mode can be implemented without any modifications to the Android operating system (Source code). There is also a sample code provided so you can test it by yourself.

Considerations before creating this kind of applications:

  • Developing for the masses? Will it be distributed through the Play store ? Not a good use case for Kiosk mode applications IMHO don’t do it, users don’t like it when you take over their phone and don’t allow them to use the phone freely. Might work for a very specific niche for example a mounted Tablet in a restaurant which displays today’s menus and recipes.
  • If you are developing it for controlled distribution, not through the Play store, then finalize the type of devices which will be used to distribute your app. Most common scenario is that it will be a cheap Chinese tablet running either Android 4.0+. Get familiar with the device first, figure out how many hardware keys are there and which ones you need to block or modify.
  • Savy users can bypass your restrictions if the devices are not physically secured. Although nothing in this world is completely secure so always watch out for surprises.

 Lets get started!

 Booting app on startup.

First of all we need to make sure your app starts automatically after booting your device.

Add the following permission as a child of the manifest element to your Android manifest:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

Now, the app has the permission to receive the RECEIVE_BOOT_COMPLETED broadcast. This message means that the phone was booted.

At next, add an intent filter to the manifest:

<receiver android:name=".BootReceiver">
    <intent-filter >
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>

Create a class called BootReceiver that extends BroadcastReceiver and add your code to the onReceivemethod to start your application. This Class will override the application’s startup behavior and set the main activity that will be launched:

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);
  }
}

 Disable back Button.

This is a simple task: Just override onBackPressed in our Activity class. Please note that we can not override onBackPressed inside fragments!

@Override
public void onBackPressed() {
    // nothing to do here   
}

 Disable power Button.

Sadly, disabling the power button is not possible without custom modification to the core of the Android operating system. Nevertheless, it is possible to detect the result of the button press and react on it.

On short power button press.

We can detect a short button press by handling the ACTION_SCREEN_OFF intent and kick the screen back to life with acquiring a wake lock

Please note that we can’t declare ACTION_SCREEN_OFF in the AndroidManifest.xml. We are only allowed to catch them while the application is running. For that reason, lets create a class called OnScreenOffReceiver that extends BroadcastReceiver and add the following code:

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);
  }
}

Now, lets reate a subclass of Application. This class will be needed to return to main activity when trying to change window or pressing opening a new app Add the following code to it:

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;
  }
}

Add the permission WAKE_LOCK to AndroidManifest.xml file:

<uses-permission android:name="android.permission.WAKE_LOCK"/>

On long power button press.

This hack is very simple but powerful. Just add the following code to our activity, it will surely prevent long press button:

@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);
  }
}

The idea is simple: in case any system dialog pops up, we kill it instantly by firing anACTION_CLOSE_SYSTEM_DIALOG broadcast.

 Disable volume Buttons.

If necessary, we can easily deactivate the volume buttons by consuming both button calls. Just override dispatchKeyEvent in our Activity and handle the volume buttons:

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);
  }
}

 Disable home Button and detect when a new app is opened.

Since Android 4.x.x we have no effective method to deactivate the home button. That is the reason why we need another little hack. In general the idea is to detect when a new application is in foreground and restart your activity immediately.

At first create a class called KioskService that extends Service and add the following snippet:

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;
  }
}

Add the following method in the AppContext class to start the service via application context creation.

@Override
public void onCreate() {
  super.onCreate();
  instance = this;
  registerKioskModeScreenOffReceiver();
  startKioskService();  // add this
}
private void startKioskService() { // ... and this method
  startService(new Intent(this, KioskService.class));
}

Last, add the service declaration and the permission for retrieving the foreground process to the AndroidManifest.xml file:

<service android:name=".KioskService" android:exported="false"/>
<uses-permission android:name="android.permission.GET_TASKS"/>

Basically, the thread checks every two seconds if your application is running in foreground. If not, the thread will immediately recreate your activity. If we want to change that interval just modify the next line:

private static final long INTERVAL = TimeUnit.SECONDS.toMillis(2)

 Prevent Screen from Dimming.

It is also very easy to keep the screen bright as long as your app is visible (also forever). You only have to add the following flag to your root layout:

android:keepScreenOn="true"

For example:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/myActivityRootLayout"
  android:layout_width="match_parent" 
  android:layout_height="match_parent"
  android:keepScreenOn="true">
  <!-- your layout -->
</RelativeLayout>

 Locking Status/Notification Bar.

We could not prevent the status appearing in full screen mode in 4.x.x devices, so made a hack which still suits the requirement i.e. block the status bar from expanding.

The easy hack is just to put an overlay over status bar and consumed all input events. It prevented the status from expanding.

note:

  • customViewGroup is custom class which extends any layout(frame,relative layout etc) and consumes touch event.
  • to consume touch event override the onInterceptTouchEvent method of the view group and return true

We need to create the customViewGroup which expands from ViewGroup, and add the following code:

public class customViewGroup extends ViewGroup {
	public customViewGroup(Context context) {
		super(context);
	}
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
	}
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.v("customViewGroup", "**********Intercepted");
		return true;
	}
}

Add the next line to AndroidManifest.xml file:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> 

And finally, from our activity inside the OnCreate() method call a new method containing the following code:

WindowManager manager = ((WindowManager) getApplicationContext()
            .getSystemService(Context.WINDOW_SERVICE));
WindowManager.LayoutParams localLayoutParams = new WindowManager.LayoutParams();
localLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
localLayoutParams.gravity = Gravity.TOP;
localLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE|
            // this is to enable the notification to recieve touch events
            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
            // Draws over status bar
            WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
    localLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
    localLayoutParams.height = (int) (50 * getResources()
            .getDisplayMetrics().scaledDensity);
    localLayoutParams.format = PixelFormat.TRANSPARENT;
    customViewGroup view = new customViewGroup(this);
    manager.addView(view, localLayoutParams);

And that’s it. Here you can find a full sample app which shows the functionality explained in this post.

Leave a Reply

Your email address will not be published. Required fields are marked *