Condividi tramite


Supporting Kiosk-Applications on Windows Mobile (Technically achievable vs. supported)

- Why unsupported

- Supported examples

- System Center Mobile Device Manager 2008

Every now and then we receive requests from ISV Application Developers regarding how to restrict users' access to the device functionality by writing a KIOSK application: many times it happens that Application Developers are asked to develop such a solution *AFTER* the final customer chose and bought all the devices to be used on the field, and many times they happen to be Windows Mobile-based, not CE. You're lucky if all the devices share the same manufacturer and model...

This brings me talking about supporting developers with problems when writing a kiosk-mode application. The Microsoft Windows *Mobile* platforms do not support a "Kiosk" type solution. It is designed from a PDA background, where the user is in control of the device rather than any one user application. There is always going to be some UI event that will cause the kiosk mode to be broken out of. This is true of Windows Mobile 2003, 5.0 and 6, and for both Pocket PC or Smartphone.

 

When a request similar to that comes from a device-manufacturer or at early stage of the "project", we usually suggest using Windows CE instead of Windows Mobile. This may involve working with a hardware vendor to get a custom embedded device that meet your needs. While the Windows Mobile platforms are not designed for Kiosk-type applications, Windows CE product can be used to build custom operating systems for embedded devices of all types. Windows CE is fully customizable, so a hardware vendor can tailor it specifically to support Kiosk usage.

 

Marcus Perryman, Mike Hall and Rabi Satter blogged about this:

So the main problem an application developer can face is that the UI will always have some Windows Messages that he\she might potentially have not trapped (see for example the “Run” dialog when tapping the clock together with the Action button of some Pocket PCs...).

I'm not stating that it's not technically achievable: there are even some 3rd party solutions on the market that demonstrate this and there are external consultants available for such matter. However, since it's conceptually something the platform was not designed\tested\documented for, then it's something Microsoft can't provide support about.

Having said that and setting the right expectations, in some special cases it might be that the contextual problem the developer is asking support about can be seen from a different perspective, without considering that the final application will be a kiosk-one. For example, it might be that (1) the developer needs support with a full-screen application, which is a documented topic in the Windows Mobile's SDKs and therefore it's completely supported. Or, it might be that (2) the developer needs help on having an application running at system startup. Or, (3) the developer might need help on knowing how to soft-reset the device (when the application is unexpectedly closed, for example). This is NOT the case when a developer wants for example to remove phone-UI on a phone-powered Windows Mobile device; or this is NOT the case when a developer wants to remove the OS taskbar altogether and\or subclass it; and so on... In such cases, even if the request came from an OEM we couldn't help because its platform would probably not pass the Windows Mobile Certification Logo Test. Again, technically achievable doesn't mean supported...

So, let's keep in mind the 3 "supported" examples above:

(1) the developer needs support with a full-screen application, which is a documented topic in the Windows Mobile's SDKs and therefore it's completely supported.

Sometimes it's not sufficient on NETCF applications to have:

 //imagine the following in Form's ctor, after InitiliazeComponents
this.FormBorderStyle = FormBorderStyle.None;
this.WindowState = FormWindowState.Maximized;
this.ControlBox = false;
this.MinimizeBox = false;

 

and sometimes it's not sufficient on native applications to invoke SHFullScreen, even if you set SetForegroundWindow before and MoveWindow afterwards:

 RECT  rc;
SetRect(&rc, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
SetForegroundWindow(hWnd); //just to make sure main window it's foreground
if (SHFullScreen(hWnd, SHFS_HIDETASKBAR | SHFS_HIDESTARTICON | SHFS_HIDESIPBUTTON))
{
    MoveWindow(hWnd, rc.left, rc.top, rc.right-rc.left, rc.bottom-rc.top, TRUE);
}

 

It might not be sufficient for many reasons, depending on the actual scenario:

a- for example you want the hardware buttons to continue working (such as volume control)

b- for example you want to remove the taskbar but maintain SIP button and therefore you need to show SIP Options dialog correctly

c- for example you have many forms and when switching among them user can see the taskbar flickering (and this undesired effect is dependent on the complexness of the form that is going to be shown, expressed in terms of numbers of child controls)

d- for example in your code you show a dialog at some point (MessageBox.Show() or frm2.ShowDialog()) and don't want the taskbar or the menubar or the SIP to appear while the dialog is shown

e- for example you want to intercept pressing hardware buttons

- ...

Considering NETCF applications (but since I'm going to talk about P/Invoke some Windowing API the same applies to native applications) the best approach I've ever found (and in the past I tried with many combinations of MoveWindow, ShowWindow, SetWindowLong, etc...), which addresses "specifications" a, b, c of the above in one shot is the one recommended by Rabi Satter in the post I mentioned above (Kiosk Pattern).

!WARNING! Remember in any case that before closing the application you must unhide the taskbar and you must consider that if your application closes unexpectedly nothing will know to unhide the taskbar again. So you can think for example of a"shell"-like application that checks if yours is running and if not launch it again, similarly to Hopper's "FocusApp" sample provided with WM6's SDK (or checks if your process is not running through OpenProcess and if so unhide the taskbar, for example):

 //
// FocusApp.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>

#define ONE_SECOND      1000
#define TEN_SECONDS     (10 * ONE_SECOND)
#define TWENTY_SECONDS  (2 * TEN_SECONDS)
#define ONE_MINUTE      (3 * TWENTY_SECONDS)
#define FIVE_MINUTES    (5 * ONE_MINUTE)
#define FIFTEEN_MINUTES (3 * FIVE_MINUTES)
 
// Adjust the following to suit your needs
#define SLEEP_TIMEOUT TEN_SECONDS
TCHAR *g_pszAppName = TEXT("\\windows\\wmPlayer.exe"); 
 
//------------------------------------------------------------------------------
int _tmain(int argc, _TCHAR* argv[])
{
    TCHAR tszTmp[MAX_PATH];
    PROCESS_INFORMATION piProcInfo; 
 
    while(TRUE)
    {
        wsprintf(tszTmp, TEXT(" ... Relaunching: %s"), g_pszAppName);
        OutputDebugString(tszTmp);
        if(! CreateProcess(g_pszAppName, NULL, NULL, NULL, FALSE, 0, NULL, 
                NULL, NULL, &piProcInfo))
        {
            goto Error;
        }
 
        // Adjust this value to suit your needs above
        Sleep(SLEEP_TIMEOUT);
    }
Error: 
    // Error condition - should never get here.
    wsprintf(tszTmp, TEXT("ERROR: could not launch %s, last error: %d"), 
        g_pszAppName, GetLastError());
    OutputDebugString(tszTmp);
    return(TRUE);
}

 

For point d above the best approach I found (but there might be others, depending on the actual scenario) is to override OnActivate and OnDeactivate events. Note that OnDeactivate is raised also after closing the form, which is the point where you want to unhide the taskbar and restore usual UI. Finally, for point e you can basically use Microsoft.WindowsCE.Forms.HardwareButton class' AssociatedControl property.

The code might look like the following (provided 'as is', without error-checking - moreover I wanted to maintain Rabi Satter's methods as they were, so the code might be better accommodated):

 public Form1()
{
    InitializeComponent();

    this.FormBorderStyle = FormBorderStyle.None;
    this.WindowState = FormWindowState.Maximized;
    this.ControlBox = false;
    this.MinimizeBox = false;

    Util.HideTaskbar();
}

//Deactivate is raised when:
//  - a Messagebox is shown
//  - another form is shown
//  - SIP Options dialog is shown
//  - the application is closed
//SHFullScreen is successful only if it receives the handle of the foreground window
protected override void OnDeactivate(EventArgs e)
{
    //if the application is closing then you don't want to remove menu and
    //you want to remove associations with hardware buttons
    if (!closing) 
    {
        this.Menu = null;
        IntPtr handle = Util.GetForegroundWindow();
        Util.SHFullScreen(handle, Util.SHFS.HIDESTARTICON | Util.SHFS.HIDESIPBUTTON );
        Util.UnhideTaskbar();
    }
    base.OnDeactivate(e);
}

protected override void OnActivated(EventArgs e)
{
    this.Menu = this.mainMenu1;
    Util.SetForegroundWindow(this.Handle);
    Util.SHFullScreen(this.Handle, Util.SHFS.HIDESTARTICON | Util.SHFS.SHOWSIPBUTTON);
    Util.HideTaskbar();

    base.OnActivated(e);
}


//this is required if you want to completely restore the taskbar (with the Start button as well)
//when exiting the application
private void Form1_Closed(object sender, EventArgs e)
{
    IntPtr handle = Util.GetForegroundWindow();
    Util.SHFullScreen(handle, Util.SHFS.SHOWSIPBUTTON | Util.SHFS.SHOWSTARTICON | Util.SHFS.SHOWTASKBAR);
    Util.UnhideTaskbar();
    
    hardwareButton1.AssociatedControl = null;
    hardwareButton2.AssociatedControl = null;
    hardwareButton3.AssociatedControl = null;
    hardwareButton4.AssociatedControl = null;
    hardwareButton5.AssociatedControl = null;
    hardwareButton6.AssociatedControl = null;
}


//this is needed when you want to exit the application (Closing->Closed->Deactivate)
private bool closing = false;
private void Form1_Closing(object sender, CancelEventArgs e)
{
    closing = true;
}


private void Form1_Load(object sender, EventArgs e)
{
    hardwareButton1.AssociatedControl = this;
    hardwareButton1.HardwareKey = HardwareKeys.ApplicationKey1;
    hardwareButton2.AssociatedControl = this;
    hardwareButton2.HardwareKey = HardwareKeys.ApplicationKey2;
    hardwareButton3.AssociatedControl = this;
    hardwareButton3.HardwareKey = HardwareKeys.ApplicationKey3;
    hardwareButton4.AssociatedControl = this;
    hardwareButton4.HardwareKey = HardwareKeys.ApplicationKey4;
    hardwareButton5.AssociatedControl = this;
    hardwareButton5.HardwareKey = HardwareKeys.ApplicationKey5;
    hardwareButton6.AssociatedControl = this;
    hardwareButton6.HardwareKey = HardwareKeys.ApplicationKey6;
}

private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    switch ((HardwareKeys)e.KeyCode)
    {
        case HardwareKeys.ApplicationKey1:
            MessageBox.Show("Hardware Key 1");
            break;
        case HardwareKeys.ApplicationKey2:
            MessageBox.Show("Hardware Key 2");
            break;
        case HardwareKeys.ApplicationKey3:
            MessageBox.Show("Hardware Key 3");
            break;
        case HardwareKeys.ApplicationKey4:
            MessageBox.Show("Hardware Key 4");
            break;
        case HardwareKeys.ApplicationKey5:
            MessageBox.Show("Hardware Key 5");
            break;
        case HardwareKeys.ApplicationKey6:
            MessageBox.Show("Hardware Key 6");
            break;
    }
}

 

where Util class is:

 //see Rabi Satter's https://www.satter.org/2007/04/kiosk_pattern.html
public class Util
{
    public static int TaskbarHeight = Screen.PrimaryScreen.Bounds.Height - Screen.PrimaryScreen.WorkingArea.Height;

    public static void HideTaskbar()
    {
        IntPtr handle;
        try
        {
            // Find the handle to the Start Bar
            handle = FindWindow("HHTaskBar", null);

            // If the handle is found then hide the start bar
            if (handle != IntPtr.Zero)
            {
                // Hide the start bar
                SetWindowPos(handle, 0, 0, 0, 0, 0, SWP.SWP_HIDEWINDOW);
            }
        }
        catch
        {
            MessageBox.Show("Could not hide Start Bar.");
        }
    }

    public static void UnhideTaskbar()
    {
        IntPtr handle;
        try
        {
            // Find the handle to the Start Bar
            handle = FindWindow("HHTaskBar", null);

            // If the handle is found then show the start bar
            if (handle != IntPtr.Zero)
            {
                // Show the start bar
                SetWindowPos(handle, 0, 0, 0, Screen.PrimaryScreen.Bounds.Width, TaskbarHeight, SWP.SWP_SHOWWINDOW);
            }
        }
        catch
        {
            MessageBox.Show("Could not show Start Bar.");
        }
    } 

    
    [DllImport("aygshell.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool SHFullScreen(IntPtr hwndRequester, SHFS dwState);

    [DllImport("coredll.dll", SetLastError = true)]
    public static extern IntPtr FindWindow(string _ClassName, string _WindowName);

    [DllImport("coredll.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool SetWindowPos(IntPtr hwnd, int hwnd2, int x, int y, int cx, int cy, SWP uFlags);

    [DllImport("coredll.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool MoveWindow(IntPtr hWnd, int x, int y, int cx, int cy, bool repaint);

    [DllImport("coredll.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool SetForegroundWindow(IntPtr hWnd);

    [DllImport("coredll.dll", SetLastError = true)]
    public static extern IntPtr GetForegroundWindow();

    [Flags()]
    public enum SHFS
    {
        SHOWTASKBAR = 0x0001,
        HIDETASKBAR = 0x0002,
        SHOWSIPBUTTON = 0x0004,
        HIDESIPBUTTON = 0x0008,
        SHOWSTARTICON = 0x0010,
        HIDESTARTICON = 0x0020,
    }

    [Flags()]
    public enum SWP
    {
        SWP_ASYNCWINDOWPOS = 0x4000,
        SWP_DEFERERASE = 0x2000,
        SWP_DRAWFRAME = 0x0020,
        SWP_FRAMECHANGED = 0x0020,
        SWP_HIDEWINDOW = 0x0080,
        SWP_NOACTIVATE = 0x0010,
        SWP_NOCOPYBITS = 0x0100,
        SWP_NOMOVE = 0x0002,
        SWP_NOOWNERZORDER = 0x0200,
        SWP_NOREDRAW = 0x0008,
        SWP_NOREPOSITION = 0x0200,
        SWP_NOSENDCHANGING = 0x0400,
        SWP_NOSIZE = 0x0001,
        SWP_NOZORDER = 0x0004,
        SWP_SHOWWINDOW = 0x0040
    }
}

(2) the developer needs help on having an application running at system startup.

This is a matter of setting some registry keys under HKLM\Init as documented in Configuring the Process Boot Phase and add a call to SystemStarted() in the application. Or, an alternative approach was proposed by Mike Hall here.

(3) the developer might need help on knowing how to soft-reset the device (when the application is unexpectedly closed, for example).

The article How to: Reset the Device contains NETCF  sample code.

The upcoming "Microsoft System Center Mobile Device Manager 2008" (second quarter of 2008) will give the ability to IT administrators to control the devices connected through Exchange by using Active Directory\Group-Policies. Some of the group policies are related to prevent users to modify certain device-settings and to install\run only some specified applications. From Product Reference Guide: "[...] Mobile Device Manager allows IT administrators to either “enable” or “disable” specific applications or sets of applications. Disabled applications cannot be installed on a managed Windows Mobile device. System Center Mobile Device Manager can also set a list of enabled applications, which are the only applications that a user can install on a device". Another interesting introductive reading is the Technical Overview White Paper.

Comments

  • Anonymous
    March 01, 2008
    PingBack from http://msdnrss.thecoderblogs.com/2008/03/01/supporting-kiosk-applications-on-windows-mobile-technically-achievable-vs-supported/

  • Anonymous
    May 11, 2008
    &quot;supported&quot;?? An interesting API: GetForegroundKeyboardTarget Sometimes cases arrive to my

  • Anonymous
    June 20, 2009
    hello could you upload your sample source code ? i was still confused about the kiosk mode thanks in advanced!

  • Anonymous
    June 21, 2009
    The sample source code is precisely the one I copied above... what's your doubt precisely? Maybe we can address it.

  • Anonymous
    July 12, 2009
    HI,raffael I was still working at the fullsrceen. i wanted to have all the windows in WM6 show in Kiosk mode,so i wrote a dll and i injected the dll into the current foreground window. at last ,i control the window with windows subclass technology. however,it can not work in some windows such as tamil.exe. the menubar was still in the window.

  • Anonymous
    July 19, 2009
    > "so i wrote a dll and i injected the dll into the current foreground window" At first sight that doesn't sound like a supportable scenario.. anyway why you want to remove the menubar (the bar below) of a third application that you want to make allowed? I mean, how can it be used then?

  • Anonymous
    February 02, 2012
    Hi, Thanks for nice article. I am writing a full screen application. Everything seems to work well except 'Home' key. I am using HTC touch screen phone and when I press home key, my application goes in background and mobile's home screen comes up in foregroud. Any ideas what could have been wrong? Thanks again Braj

  • Anonymous
    February 12, 2012
    Hi Braj, I don't think you're doing anything wrong: you may experience one of the OEM-specific customizations that would prevent to completely have a kiosk-mode app on Windows Mobile. You may use Remote Spy to see exactly which windows message is generated when pressing Home Key, so that you can intercept it in your WndProc (see also my post about Subclassing). HTH, ~raffaele