Skip to main content
  1. Writing/

Dell Venue Pro – Working With The Native Layer

·1901 words

Each Windows Phone OS – powered device has its own way of communicating with its hardware. In a non-public environment, this is done through a COM (Component Object Model) layer. A DLL providing this layer is usually shipped as a part of the OS or an official OEM application. When it is distributed the second way, it is fairly easy to intercept the XAP and extract the DLL. And that’s when the experiments begin.

Dell provides probably one of the less popular Windows Phone devices. Nonetheless, Dell Venue Pro has more than decent hardware parameters and it is quite fun to see how those can be leveraged in an application built with bits outside the official SDK.

OurCOM.dll #

Image lost since transition to new blog

It all starts with OurCOM.dll – the library that is shipped by Dell as a part of their EM (Engineering Mode) application. Unlike their competitors from Samsung and LG, the default application is written quite poorly and all interactions with the COM layer are done through plain-text commands that are later parsed to get correct values. Quite an interesting (unusual and unusable?) choice, but if that’s all we have, there is no choice.

WARNING: Before going any further with the experiments described in this article, remember that by using the presented code snippets you might:

  • Brick your phone
  • Erase all content present on the device
  • Render the phone un-usable for unknown periods of time

That being said, I am assuming no liability or responsibility for any of the actions you undertake with your Windows Phone device while following my instructions in here.

The library itself should be added to the Windows Phone application project you plan on working with. In my case, it is set to Content and Copy Always – obviously it is not going to be a standard reference because of the platform restrictions.

Preparing the application #

Also, WPInteropManifest.xml is needed in order to enable interop calls. Its contents are pretty basic:

<?xml version="1.0" encoding="UTF-8"?>

<Interop>
</Interop>

Don’t forget about the proper CAPs in WMAppManifest.xml:

Image lost since transition to new blog

ID_CAP_INTEROPSERVICES is crucial for any COM interop code to work. Last but not least, you will need Microsoft.Phone.InteropServices.dll for ComImport to be available. You can get to the library via Reflection or you can download it as a part of the dumped GAC here.

COM Connection Fundamentals #

First, the default program components (read: classes and interfaces):

[ComImport, Guid ("E2A22F82-0E6A-4ee9-A56D-2677EBE81F2E"), ClassInterface (ClassInterfaceType.None)]
public class COMInteropClass { }

[ComImport, InterfaceType (ComInterfaceType.InterfaceIsIUnknown), Guid ("795CA8D5-08FB-47ca-ABC3-3BB6A9684F4D")]
internal interface ICOMInterop {
    void Connect ();
    void Disconnect ();
    void Send (string text);
    void Receive (StringBuilder text);
    void DriverConnect ();
    void DriverDisconnect ();
    void DriverQuery (string input, StringBuilder output);
}

public interface ICOMConnector {
    bool Connect ();
    bool Disconnect ();
    void StartPolling (string InputCommand, PollingDataHandler Datahandler, int Period);
    void StopPolling (string InputCommand);
    Result SendCommand (string InputCommand, int sec);
    Result SendCommand (string InputCommand, int sec, bool repeat);
}

public delegate void PollingDataHandler (Result result);

public class Result {
    public string Content;
    public static int IsEmpty;
    public static int IsFailed;
    public string FunctionName;
    public static int IsInvalid;
    public int IsSuccessful;
    public static string Timeout;
    public static string IsUnknownCommand;
    public static int SUCCESS;
    public static int TIMEOUT;
    public static int UNKNOWCOMMAND;
}

public class PollingTimer : DispatcherTimer {
    public PollingDataHandler PollingHandler = null;
    public static int PollingPeriod = 50;
    public string QueryString = "";
    public bool SendOnlyOnce = false;
}

public class COMHelper {
    public const uint E_UNEXPECTED = 0x8000ffff;
    public const int POLLING_TIMER_IN_SELF = 0;
    public const int POLLING_TIMER_IN_SERVER = 1;
    public const int POLLING_TIMER_WAY = 0;
    public const uint S_OK = 0;
    public const string AssemblyName = "OurCOM.dll";
    public const string AssemblyGuid = "E2A22F82-0E6A-4ee9-A56D-2677EBE81F2E";

    public static string ExtractFunctionName (string Command) {
        string[] strArray = null;
        strArray = ParseCommandString (Command);
        if (strArray[0].CompareTo ("EM") != 0) {
            return null;
        }
        return strArray[1];
    }

    public static string[] ParseCommandString (string Command) {
        return Command.Split (new char[] { ':', ';' });
    }

    public static List<Result> ParsingResults (string stringFromCOM) {
        List<Result> list = new List & lt;
        Result & gt;
        ();
        string[] strArray = stringFromCOM.Split (new char[] { ';' });
        foreach (string str in strArray) {
            Result item = GetResult (str);
            if (item != null) {
                list.Add (item);
            }
        }
        return list;
    }

    public static Result ParseSingleResult (string stringFromCOM) {
        string[] strArray = stringFromCOM.Split (new char[] { ';' });
        if (strArray.Length & gt; = 2) {
            Debug.WriteLine ("Native response: " + stringFromCOM);
        }
        Result result = GetResult (strArray[0]);
        if (result == null) {
            Debug.WriteLine ("Nothing to convert.");
        }
        return result;
    }

    public static bool RegisterCOM () {
        return (ComBridge.RegisterComDll ("OurCOM.dll", new Guid ("E2A22F82-0E6A-4ee9-A56D-2677EBE81F2E")) == 0);
    }

    public static string ReplaceKeywords (string str) {
        string str2 = str;
        return str2.Replace (";", @"\a").Replace (":", @"\b").Replace (",", @"\c");
    }

    public static string RestoreKeywords (string str) {
        string str2 = str;
        return str2.Replace (@"\a", ";").Replace (@"\b", ":").Replace (@"\c", ",");
    }

    public static Result GetResult (string resultString) {
        Result result = null;
        string[] strArray = ParseCommandString (resultString);
        if (strArray[0].CompareTo ("EM") != 0) {
            return null;
        }
        result = new Result ();
        result.FunctionName = strArray[1];
        if (strArray.Length & gt; = 3) {
            if (strArray.Length & gt; = 4) {
                result.Content = strArray[3];
            } else {
                result.Content = strArray[2];
            }
        }
        result.IsSuccessful = Result.SUCCESS;
        return result;
    }
}

public class EMToComByDriver : ICOMConnector {
    private Dictionary<string, PollingTimer> handlerTable = new Dictionary<string, PollingTimer> ();
    private ICOMInterop COMInterface = null;
    private COMInteropClass helloObj = null;
    private StringBuilder outputBuf = new StringBuilder (0x400);

    private void AddPollingTimer (string functionName, PollingTimer pollingTimer) {
        PollingTimer timer = null;
        if (this.handlerTable.TryGetValue (functionName, out timer)) {
            timer.Stop ();
            this.handlerTable.Remove (functionName);
        }
        this.handlerTable.Add (functionName, pollingTimer);
    }

    public bool Connect () {
        try {
            if (COMHelper.RegisterCOM ()) {
                this.helloObj = new COMInteropClass ();
                this.COMInterface = (ICOMInterop) this.helloObj;
                this.COMInterface.DriverConnect ();
            } else {
                Debug.WriteLine ("COM Registration Failed");
                return false;
            }
        } catch (Exception exception) {
            Debug.WriteLine ("Exception: " + exception.Message);
            return false;
        }
        return true;
    }

    public bool Disconnect () {
        try {
            this.COMInterface.DriverDisconnect ();
        } catch (Exception exception) {
            Debug.WriteLine ("Exception: " + exception.Message);
            return false;
        }
        return true;
    }

    public void StartPolling (string inputCommand, PollingDataHandler handler, int period) {
        string functionName = null;
        PollingTimer pollingTimer = new PollingTimer ();
        functionName = COMHelper.ExtractFunctionName (inputCommand);
        if (functionName == null) {
            Debug.WriteLine ("Error : command error : " + inputCommand);
        } else {
            pollingTimer.PollingHandler = handler;
            pollingTimer.Tick += new EventHandler (this.SendCommand);
            pollingTimer.Interval = new TimeSpan (0, 0, 0, 0, period);
            pollingTimer.QueryString = inputCommand;
            pollingTimer.SendOnlyOnce = false;
            pollingTimer.Start ();
            this.AddPollingTimer (functionName, pollingTimer);
        }
    }

    private void SendCommand (object sender, EventArgs e) {
        PollingTimer timer = (PollingTimer) sender;
        Result result = null;
        try {
            this.COMInterface.DriverQuery (timer.QueryString, this.outputBuf);
            string stringFromCOM = this.outputBuf.ToString ();
            Debug.WriteLine ("COM Response: " + stringFromCOM);
            result = COMHelper.ParseSingleResult (stringFromCOM);
        } catch (Exception exception) {
            Debug.WriteLine ("COM Query Error: " + exception.Message);
        }
        if (result == null) {
            Debug.WriteLine ("Query command failed: " + timer.QueryString);
        } else {
            timer.PollingHandler (result);
        }
    }

    public void StopPolling (string inputCommand) {
        string key = null;
        PollingTimer timer = null;
        key = COMHelper.ExtractFunctionName (inputCommand);
        if (key == null) {
            Debug.WriteLine ("Error: " + inputCommand);
        } else if (this.handlerTable.TryGetValue (key, out timer)) {
            timer.Stop ();
            this.handlerTable.Remove (key);
        }
    }

    public Result SendCommand (string InputCommand, int sec) {
        return this.SendCommand (InputCommand, sec, false);
    }

    public Result SendCommand (string InputCommand, int sec, bool repeat) {
        Result result = null;
        string str = COMHelper.ExtractFunctionName (InputCommand);
        try {
            this.COMInterface.DriverQuery (InputCommand, this.outputBuf);
            result = COMHelper.ParseSingleResult (this.outputBuf.ToString ());
        } catch (Exception exception) {
            Debug.WriteLine ("calling COM query error(driver) : " + exception.Message);
        }
        if (result == null) {
            result = new Result ();
            result.FunctionName = str;
            result.IsSuccessful = Result.IsFailed;
        }
        return result;
    }
}

These are parts of the default connecting code used in the EM application. I modified it a bit for proper naming and more readable results (at least it will be possible to know what’s going on). EMToComByDriver seems to be the connecting class that works. There is one that relies on sockets, but it appears that developers forgot to properly integrate it so it fails to actually do anything on the phone.

A lot of the code above is dedicated to parsing commands and COM responses. I am not entirely sure why the guys that wrote the COM DLL didn’t want to use direct function cross-invocation, but alas – I have no real choice here. At least right now.

In my application, I am creating an instance of ICOMConnector and instantiating a new EMToComByDriver that is assigned to the interface when the page is initialized:

ICOMConnector tm;
 
public MainPage()
{
    InitializeComponent();
    tm = new EMToComByDriver();
    tm.Connect();
}

The call to Connect actually invokes the COM registration process and stores the current instance of the COMInteropClass for later reuse. It is time to learn more about commands now!

String commands #

REGISTRY #

One of the gray areas on a Dell Venue Pro device is the registry. The problem is – apparently, there is no hook in the native DLL that allows modification of the system registry (although an obvious attempt to link it is there). That being said, to get a registry key, a command like this can be sent:

tm.SendCommand("EM:REGISTRY_GET:" + @"ControlPanel\themes\0" + "," + "BaseThemeColor" + "," + "REG_SZ" + "," + "2" + ";", 1, false);

The command itself has the following format (in case commas make it difficult to read):

EM:REGISTRY_GET:ControlPanel\themes\0,BaseThemeColor,REG_SZ,2;

Here is yet another interesting (and inexplicable) decision – use integer identifiers for key stores instead of direct paths. The ending integer is nothing else but a way to show what is the source of the actual key you are trying to get. These are:

  • HKEY_CLASSES_ROOT – 0
  • HKEY_CURRENT_USER – 1
  • HKEY_LOCAL_MACHINE – 2
  • HKEY_USERS – 3

REG_SZ defines the key type to be read and it pretty much determines the value type. These can be:

  • REG_DWORD
  • REG_SZ
  • REG_MULTI_SZ
  • REG_BINARY

The SET command works the same way, but with an included value to set:

string setCommand= "EM:REGISTRY_SET:" + @"ControlPanel\themes\0" + "," + "BaseThemeColor" + "," + "REG_SZ" + "," + "VALUE_GOES_HERE" + "2" + ";";

The response (instance of Result) is not informative:

Image lost since transition to new blog

Considering the values that are seen in the screenshot above, it is safe to assume that the SET command is not implemented in the native layer.

IDEA TO WORK ON: A way to push and execute REG files (like it is in regular Windows systems) to update the registry values.

WORK MODES #

These define whether the phone works in Composite or Zune mode. The Composite mode is usually necessary to set the phone up for tethering. Zune mode is the default work mode that allows the phone to be synced with the PC and updated.

The command to set the phone to Composite mode:

tm.SendCommand("EM:NV_50002_VALUE:1;", 1, false);

To switch back to Zune mode:

tm.SendCommand("EM:NV_50002_VALUE:3;", 1, false);

Once again proves that the values send back and forth are quite cryptic, but they work.

NOTE: Once you invoke one of the commands that causes the work mode to be changed, the phone will automatically restart.

SECOND NOTE: You will not be able to read the Result returned by the COM invocation once the command is triggered.

Make sure that you will have access to the mode-switching application once the phone reboots. You might need to switch modes back eventually. If not, you can always use the default EM app that cannot be removed without significant effort.