Wenchao Jiang's BlogJekyll2015-08-01T10:45:53+00:00http://wenchaojiang.github.io/Wenchao Jianghttp://wenchaojiang.github.io/jiang.wenchao.goo@email.comhttp://wenchaojiang.github.io/blog/realise-Android-kiosk-mode2015-08-01T19:39:55+00:002015-08-01T19:39:55+00:00Wenchao Jianghttp://wenchaojiang.github.iojiang.wenchao.goo@email.com
<h3 id="introduction">Introduction</h3>
<p>Motivation of this article is to write down how to realise a Kiosk mode App, through which a user locked in and can not mess around with other system functionalities. My approach does not require root.</p>
<p>My kiosk mode has several requirement:</p>
<ol>
<li>lock the user in the app.</li>
<li>boot into the app when system start up.</li>
<li>the app should be full screen.</li>
<li>keep system awake all time.</li>
<li>disable volume buttons.</li>
</ol>
<p>My approaches:</p>
<ol>
<li>use app pinning feature newly introduced in SDK level 22 to lock the user in the app.</li>
<li>set the app as a home intent, so system can boot into the app.</li>
<li>make the app full screen with immersive mode of activity.</li>
<li>keep system awake through sdk</li>
<li>disable the volume buttons through sdk</li>
</ol>
<p>Next I will introduce how to do it step by step, all the template code is available in the repo:
<a href="https://github.com/wenchaojiang/AndroidKioskModeTemplate">AndroidKioskModeTemplate</a></p>
<h3 id="configurations">Configurations</h3>
<p>You need target Sdk level 21 for screen pinning feature.</p>
<h3 id="screen-pinning">Screen pinning</h3>
<p>Andriod 5.0 introduces the screen pinning feature, as its documentation said:
Once your app activates screen pinning, users cannot see notifications, access other apps, or return to the home screen, until your app exits the mode.</p>
<p>The screen pinning mode can be simply calling <code>startLockTask()</code> in your activity and exit by calling <code>stopLockTask()</code>.</p>
<p>However, when <code>startLockTask()</code> called, system will ask user the permission for enterring pinning mode and user can still exit the pinning mode by touch “back and task” buttons all together. The is not “true” Kiosk that lock user in. To enable a true kiosk mode, your app need to call the <code>startLockTask()</code> as a device owner.</p>
<p>To set your app as device owner, you need to implement a <code>DeviceAdminReceiver</code> class <code>MyAdmin</code>, and use the dpm tool under adb shell</p>
<h4 id="implementing-a-deviceadminreceiver">Implementing a DeviceAdminReceiver</h4>
<p>First, create a class inheriting the <code>DeviceAdminReceiver</code>, no implemenation is required.</p>
<div class="highlight"><pre><code class="language-java" data-lang="java"><span class="kn">package</span> <span class="n">wenchao</span><span class="o">.</span><span class="na">kiosk</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">android.app.admin.DeviceAdminReceiver</span><span class="o">;</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyAdmin</span> <span class="kd">extends</span> <span class="n">DeviceAdminReceiver</span><span class="o">{</span>
<span class="o">}</span></code></pre></div>
<p>Second, declare the receiver in <code>Manifest.xml</code>, in your <code><applicatio></code>n element</p>
<div class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="nt"><receiver</span> <span class="na">android:name=</span><span class="s">"wenchao.kiosk.MyAdmin"</span>
<span class="na">android:label=</span><span class="s">"@string/sample_device_admin"</span>
<span class="na">android:description=</span><span class="s">"@string/sample_device_admin_description"</span>
<span class="na">android:permission=</span><span class="s">"android.permission.BIND_DEVICE_ADMIN"</span><span class="nt">></span>
<span class="nt"><meta-data</span> <span class="na">android:name=</span><span class="s">"android.app.device_admin"</span> <span class="na">android:resource=</span><span class="s">"@xml/my_admin"</span> <span class="nt">/></span>
<span class="nt"><intent-filter></span>
<span class="nt"><action</span> <span class="na">android:name=</span><span class="s">"android.app.action.DEVICE_ADMIN_ENABLED"</span> <span class="nt">/></span>
<span class="nt"></intent-filter></span>
<span class="nt"></receiver></span>
<span class="nt"><uses-permission</span> <span class="na">android:name=</span><span class="s">"android.permission.MANAGE_DEVICE_ADMINS"</span> <span class="nt">/></span></code></pre></div>
<p>The following line of meta-data need to match a xml resource file, in this case <code>my_admin</code></p>
<p><code> <meta-data android:name="android.app.device_admin" android:resource="@xml/my_admin" /></code></p>
<p>The content of <code>my_admin.xml</code></p>
<div class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="nt"><device-admin</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span><span class="nt">></span>
<span class="nt"><uses-policies></span>
<span class="nt"><limit-password</span> <span class="nt">/></span>
<span class="nt"><watch-login</span> <span class="nt">/></span>
<span class="nt"><reset-password</span> <span class="nt">/></span>
<span class="nt"><force-lock</span> <span class="nt">/></span>
<span class="nt"><wipe-data</span> <span class="nt">/></span>
<span class="nt"></uses-policies></span>
<span class="nt"></device-admin></span></code></pre></div>
<h4 id="set-your-app-as-device-owner">Set your app as device owner</h4>
<p>Now you can set your app as deivce own with dpm command under adb shell.</p>
<div class="highlight"><pre><code class="language-java" data-lang="java"><span class="n">adb</span> <span class="n">shell</span>
<span class="n">dpm</span> <span class="n">set</span><span class="o">-</span><span class="n">device</span><span class="o">-</span><span class="n">owner</span> <span class="n">wenchao</span><span class="o">.</span><span class="na">kiosk</span><span class="o">/.</span><span class="na">MyAdmin</span></code></pre></div>
<p>Replace wenchao.kiosk with your package name. <code>/.MyAdmin</code> is your receiver class, if your receiver class is in different package of you app package, specify full path such as <code>wenchao.kiosk/other.package.name.MyAdmin</code></p>
<p>Now, before you call <code>startLockTask()</code>, you need to allow the your app to pin the screen as device owner</p>
<div class="highlight"><pre><code class="language-java" data-lang="java"><span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">onCreate</span><span class="o">(</span><span class="n">Bundle</span> <span class="n">savedInstanceState</span><span class="o">)</span> <span class="o">{</span>
<span class="kd">super</span><span class="o">.</span><span class="na">onCreate</span><span class="o">(</span><span class="n">savedInstanceState</span><span class="o">);</span>
<span class="c1">// Set the app into full screen mode</span>
<span class="n">getWindow</span><span class="o">().</span><span class="na">getDecorView</span><span class="o">().</span><span class="na">setSystemUiVisibility</span><span class="o">(</span><span class="n">flags</span><span class="o">);</span>
<span class="c1">//Following code allow the app packages to lock task in true kiosk mode</span>
<span class="n">setContentView</span><span class="o">(</span><span class="n">wenchao</span><span class="o">.</span><span class="na">kiosk</span><span class="o">.</span><span class="na">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">activity_lock_activity</span><span class="o">);</span>
<span class="c1">// get policy manager</span>
<span class="n">DevicePolicyManager</span> <span class="n">myDevicePolicyManager</span> <span class="o">=</span> <span class="o">(</span><span class="n">DevicePolicyManager</span><span class="o">)</span> <span class="n">getSystemService</span><span class="o">(</span><span class="n">Context</span><span class="o">.</span><span class="na">DEVICE_POLICY_SERVICE</span><span class="o">);</span>
<span class="c1">// get this app package name</span>
<span class="n">ComponentName</span> <span class="n">mDPM</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">ComponentName</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">MyAdmin</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">myDevicePolicyManager</span><span class="o">.</span><span class="na">isDeviceOwnerApp</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">getPackageName</span><span class="o">()))</span> <span class="o">{</span>
<span class="c1">// get this app package name</span>
<span class="n">String</span><span class="o">[]</span> <span class="n">packages</span> <span class="o">=</span> <span class="o">{</span><span class="k">this</span><span class="o">.</span><span class="na">getPackageName</span><span class="o">()};</span>
<span class="c1">// mDPM is the admin package, and allow the specified packages to lock task</span>
<span class="n">myDevicePolicyManager</span><span class="o">.</span><span class="na">setLockTaskPackages</span><span class="o">(</span><span class="n">mDPM</span><span class="o">,</span> <span class="n">packages</span><span class="o">);</span>
<span class="n">startLockTask</span><span class="o">();</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="n">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="n">getApplicationContext</span><span class="o">(),</span><span class="s">"Not owner"</span><span class="o">,</span> <span class="n">Toast</span><span class="o">.</span><span class="na">LENGTH_LONG</span><span class="o">).</span><span class="na">show</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span></code></pre></div>
<p>Now, the home button in navigation disappear and user have no way to go out of your app. The back button is there, but it will do nothing</p>
<figure>
<img src="/images/kiosk-lock.png" alt="image" />
</figure>
<h3 id="boot-into-your-kiosk-app">Boot into your kiosk app</h3>
<p>Specify your activity as a HOME intent by modifying intent-filter of your activity in <code>Manifest.xml</code></p>
<div class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="nt"><intent-filter></span>
<span class="nt"><action</span> <span class="na">android:name=</span><span class="s">"android.intent.action.MAIN"</span> <span class="nt">/></span>
<span class="nt"><category</span> <span class="na">android:name=</span><span class="s">"android.intent.category.HOME"</span> <span class="nt">/></span>
<span class="nt"><category</span> <span class="na">android:name=</span><span class="s">"android.intent.category.DEFAULT"</span> <span class="nt">/></span>
<span class="nt"></intent-filter></span></code></pre></div>
<p>Then, you need to manually replace default system launcher with your app by going to <code>Settings > Home</code> , and choice your app as the home app. These only needs to be down once.</p>
<p>Once done, if you reboot the system, you app will be booted up and pin the screen. User have not no way to access the other part of your system. Note, if your system crash, the default andriod lanucher can kick in.</p>
<h3 id="full-screen">Full screen</h3>
<p>Visible navigation bars are not ideal for my project, so I decided to hide it. You can do it by setting some system window flags.</p>
<div class="highlight"><pre><code class="language-java" data-lang="java"><span class="kd">final</span> <span class="kt">int</span> <span class="n">flags</span> <span class="o">=</span> <span class="n">View</span><span class="o">.</span><span class="na">SYSTEM_UI_FLAG_LAYOUT_STABLE</span>
<span class="o">|</span> <span class="n">View</span><span class="o">.</span><span class="na">SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION</span>
<span class="o">|</span> <span class="n">View</span><span class="o">.</span><span class="na">SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN</span>
<span class="o">|</span> <span class="n">View</span><span class="o">.</span><span class="na">SYSTEM_UI_FLAG_HIDE_NAVIGATION</span>
<span class="o">|</span> <span class="n">View</span><span class="o">.</span><span class="na">SYSTEM_UI_FLAG_FULLSCREEN</span>
<span class="o">|</span> <span class="n">View</span><span class="o">.</span><span class="na">SYSTEM_UI_FLAG_IMMERSIVE_STICKY</span><span class="o">;</span>
<span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">onCreate</span><span class="o">(</span><span class="n">Bundle</span> <span class="n">savedInstanceState</span><span class="o">)</span> <span class="o">{</span>
<span class="kd">super</span><span class="o">.</span><span class="na">onCreate</span><span class="o">(</span><span class="n">savedInstanceState</span><span class="o">);</span>
<span class="cm">/* Set the app into full screen mode */</span>
<span class="n">getWindow</span><span class="o">().</span><span class="na">getDecorView</span><span class="o">().</span><span class="na">setSystemUiVisibility</span><span class="o">(</span><span class="n">flags</span><span class="o">);</span>
<span class="c1">//leave out other code ...</span>
<span class="o">}</span></code></pre></div>
<p>With my Approach, a transient navigation bar will still be visible when user swipe on the screen from top or button of the screen. It will disappear after 1-2 seconds.</p>
<figure>
<img src="/images/kiosk-fullscreen.png" alt="image" />
</figure>
<h3 id="keep-device-awake">Keep device awake</h3>
<p>This can be simply done by putting a line in <code>Manifest.xml</code>, under your activity.
<code>android:keepScreenOn="true"</code></p>
<h3 id="disable-volume-buttons">Disable volume buttons</h3>
<p>This can be down by trapping volume up and down event.</p>
<div class="highlight"><pre><code class="language-java" data-lang="java"><span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">onKeyDown</span><span class="o">(</span><span class="kt">int</span> <span class="n">keyCode</span><span class="o">,</span> <span class="n">KeyEvent</span> <span class="n">event</span><span class="o">){</span>
<span class="k">if</span> <span class="o">(</span><span class="n">keyCode</span> <span class="o">==</span> <span class="n">KeyEvent</span><span class="o">.</span><span class="na">KEYCODE_VOLUME_UP</span><span class="o">){</span>
<span class="n">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="s">"Volume button is disabled"</span><span class="o">,</span> <span class="n">Toast</span><span class="o">.</span><span class="na">LENGTH_SHORT</span><span class="o">).</span><span class="na">show</span><span class="o">();</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">keyCode</span> <span class="o">==</span> <span class="n">KeyEvent</span><span class="o">.</span><span class="na">KEYCODE_VOLUME_DOWN</span><span class="o">){</span>
<span class="n">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="s">"Volume button is disabled"</span><span class="o">,</span> <span class="n">Toast</span><span class="o">.</span><span class="na">LENGTH_SHORT</span><span class="o">).</span><span class="na">show</span><span class="o">();</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">return</span> <span class="kd">super</span><span class="o">.</span><span class="na">onKeyDown</span><span class="o">(</span><span class="n">keyCode</span><span class="o">,</span> <span class="n">event</span><span class="o">);</span>
<span class="o">}</span></code></pre></div>
<p>You programmatically set the volumes to max or mute as well</p>
<div class="highlight"><pre><code class="language-java" data-lang="java"><span class="kd">private</span> <span class="kt">void</span> <span class="nf">setVolumMax</span><span class="o">(){</span>
<span class="n">AudioManager</span> <span class="n">am</span> <span class="o">=</span> <span class="o">(</span><span class="n">AudioManager</span><span class="o">)</span> <span class="n">getSystemService</span><span class="o">(</span><span class="n">Context</span><span class="o">.</span><span class="na">AUDIO_SERVICE</span><span class="o">);</span>
<span class="n">am</span><span class="o">.</span><span class="na">setStreamVolume</span><span class="o">(</span>
<span class="n">AudioManager</span><span class="o">.</span><span class="na">STREAM_SYSTEM</span><span class="o">,</span>
<span class="n">am</span><span class="o">.</span><span class="na">getStreamMaxVolume</span><span class="o">(</span><span class="n">AudioManager</span><span class="o">.</span><span class="na">STREAM_SYSTEM</span><span class="o">),</span>
<span class="mi">0</span><span class="o">);</span>
<span class="o">}</span></code></pre></div>
<h3 id="disable-power-button">Disable power button</h3>
<p>Ideally I want to do this, but it seems not easy without modification of firmwares.</p>
<p><a href="http://wenchaojiang.github.io/blog/realise-Android-kiosk-mode/">Making an Android Kiosk App.</a> was originally published by Wenchao Jiang at <a href="http://wenchaojiang.github.io">Wenchao Jiang's Blog</a> on August 01, 2015.</p>