Wednesday, December 12, 2012

Robot Work - Reducing the Windows Phone Localization Friction a bit

Windows Phone 8 now supports 3 different screen resolutions which is very nice since there are some great high resolution devices out there and the user has the choice and isn't forced into a one fits all model. However  there's also a downside for Windows Phone developers since now also 3 times the amount of screenshots have to be submitted to the Store. If an app has localized versions this adds up very quickly. For example, Pictures Lab supports 11 languages and the soon-to-released v5 update for WP8 also all 3 resolutions: 11 x 3 x 8 screenshots = 264! That's a crazy amount of mindless click-through work to take all those screenshots, not to mention the Dev Center submission process.
Unfortunately there is no automated way or better support by the tools, so one has to do this ridiculous amount of manual work.  In a future version of the tools I'd really love to see some kind of macro recorder which records certain steps including screenshot actions and can then use the recorded steps to play this for all supported app languages and generate the remaining screenshots automatically.

In this quick post I want to share some practices I used in order to reduce the work at least a bit for the Pictures Lab v5 update:
  1. Only create screenshots for the 720p (720 x 1280) and the WXGA (768 x 1280) resolution. The WVGA (480 x 800) screenshots can be generated from the WXGA images by scaling those down by 1.6. In contrast to upscaling, are the downscaling artifacts hardly visible if a good algorithm is used. I used my favorite image viewer IrfanView and its batch processing feature which can scale down by the longest side, rename by a pattern, convert, etc.



  2. Add some code to easily switch between UI languages in your app so you don't only rely on the system language and need to reboot the emulator to switch languages. An app's UI language can be set globally by using
    System.Threading.Thread.CurrentThread.CurrentUICulture
    For example:
    System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo("de-DE");
    in the App.xaml.cs Launching event.

  3. Use a macro recorder / scripting to save a bit time with the repetitive tasks of taking screenshots only with different UI languages. I used a little free tool called Mouse Recorder Pro. This tool records your mouse and keyboard actions and can then play it back also with up to 40% faster playback speed. Of course this doesn't work for all apps and only when using the emulators, esp. uncontrollable live content would likely not work, but it worked for my scenario quite well and saved me from creating all 264 screenshots manually. In fact I watched the magic ghost mouse do its job most of the time and worked on other stuff.
    A few gotchas when using the Mouse Recorder Pro with the WP emulators:

    1. Don't click on Pivot headers and rather use the swipe gesture to change between Pivot items. The header texts will have different lengths in different languages.

    2. Try to avoid the bounce effect of the ScrollViewer, it would result in random offsets and the Mouse Recorder playback will then have non-matching offsets recorded.

    3. Do a dummy save in the destination folder for the screenshots before each playback so the playback will save the files to the right folder. This will avoid file name collisions which would need manual input.
Of course there's a lot of room for improvements, but maybe the described practices help to reduce the friction that comes with localization at least a bit until a better solution is known.


Thursday, November 1, 2012

WACK ARM

If you are developing Windows 8 Store apps you should really test those on low-powered x86 and ARM devices. The difference to your high-end x64 developer machine can be dramatically, esp. if you use a non-default design.
Testing on ARM is important and Tim Heuer wrote a nice post which shows how to remotely deploy, debug and profile on ARM devices right from Visual Studio 2012. Another nice thing which performs a couple of automated tests is the Windows App Certification Kit (WACK). Unfortunately is the WACK tool for ARM hidden inside the Windows 8 SDK, but here's how to find it:

  1. Download the Windows 8 SDK web setup from here and run it.
  2. Choose to download the SDK files for offline usage.
  3. On the "Select the features you want to download" screen it's enough if you only select the "Windows App Certification Kit" checkbox.
  4. After the download is finished, you will find an "Installers" subfolder in the download target folder.
  5. Copy the "Windows App Certification Kit arm-arm_en-us.msi" from the "Installers" folder to an USB key or SkyDrive.
  6. Open the "Windows App Certification Kit arm-arm_en-us.msi" on your ARM device and install the WACK on it. 
  7. Search for "Windows App Cert Kit" on your ARM device using the Search Charm and run the WACK.

Tuesday, October 30, 2012

Faster! - WriteableBitmapEx for Windows Phone 8 and WinRT Updated

The Windows Phone 8 SDK is now available and with that WriteableBitmapEx was updated too. The performance of the WinRT XAML version was also improved dramatically and an update is highly recommended.
WriteableBitmapEx is available for 4 platforms: Windows Phone 8 and 7, WPF, Silverlight and Windows Store WinRT .NET XAML.
You can download the binaries here or via the NuGet packageThe packages contain the WriteableBitmapEx binaries. As usual all samples and the source code can be found in the repository.

Sunday, October 28, 2012

Know Your Users - Windows 8 Store App System Info

This is just a quick a post to provide a little class for Windows 8 Store app development. The SystemInformation class gathers some useful information about the current system and can dump those as a string. I usually have such a class for all the platforms I work on and it's very helpful for logging and error analysis, but it also serves as a list of all available system information which are scattered in different APIs. The info can also be used to dynamically adapt the app, etc.

Here's the code I have so far, you can also download the complete class here.

public static async Task Dump(bool shouldDumpCompleteDeviceInfos = false)
{
   var builder = new StringBuilder();
   var packageId = Windows.ApplicationModel.Package.Current.Id;
   var clientDeviceInformation = new EasClientDeviceInformation();
    
   // Get hardware Id
   var token = HardwareIdentification.GetPackageSpecificToken(null);
   var stream = token.Id.AsStream();
   string hardwareId;
   using (var reader = new BinaryReader(stream))
   {
      var bytes = reader.ReadBytes((int)stream.Length);
      hardwareId = BitConverter.ToString(bytes);
   }

   builder.AppendLine("***** System Infos *****");
   builder.AppendLine();
#if DEBUG
   builder.AppendLine("DEBUG");
   builder.AppendLine();
#endif
   builder.AppendFormat("Time: {0}", DateTime.Now.ToUniversalTime().ToString("r"));
   builder.AppendLine();
   builder.AppendFormat("App Name: {0}", packageId.Name);
   builder.AppendLine();
   builder.AppendFormat("App Version: {0}.{1}.{2}.{3}", packageId.Version.Major, packageId.Version.Minor, packageId.Version.Build, packageId.Version.Revision);
   builder.AppendLine();
   builder.AppendFormat("App Publisher: {0}", packageId.Publisher);
   builder.AppendLine();
   builder.AppendFormat("Supported Package Architecture: {0}", packageId.Architecture);
   builder.AppendLine();
   builder.AppendFormat("Installed Location: {0}", Windows.ApplicationModel.Package.Current.InstalledLocation.Path);
   builder.AppendLine();
   builder.AppendFormat("Store App Id: {0}", CurrentApp.AppId);
   builder.AppendLine();
   if (CurrentApp.LicenseInformation.IsActive)
   {
      var listingInformation = await CurrentApp.LoadListingInformationAsync();
      builder.AppendFormat("Store Current Market: {0}", listingInformation.CurrentMarket);
      builder.AppendLine();
   }
   builder.AppendFormat("Culture: {0}", CultureInfo.CurrentCulture);
   builder.AppendLine();
   builder.AppendFormat("OS: {0}", clientDeviceInformation.OperatingSystem);
   builder.AppendLine(); 
   builder.AppendFormat("System Manufacturer: {0}", clientDeviceInformation.SystemManufacturer);
   builder.AppendLine();
   builder.AppendFormat("System Product Name: {0}", clientDeviceInformation.SystemProductName);
   builder.AppendLine();
   builder.AppendFormat("System Sku: {0}", clientDeviceInformation.SystemSku);
   builder.AppendLine();
   builder.AppendFormat("System Name: {0}", clientDeviceInformation.FriendlyName);
   builder.AppendLine();
   builder.AppendFormat("System ID: {0}", clientDeviceInformation.Id);
   builder.AppendLine();
   builder.AppendFormat("Hardware ID: {0}", hardwareId);
   builder.AppendLine();
   builder.AppendFormat("User Display Name: {0}", await UserInformation.GetDisplayNameAsync());
   builder.AppendLine();
   builder.AppendFormat("Window Bounds w x h: {0} x {1}", Window.Current.Bounds.Width, Window.Current.Bounds.Height);
   builder.AppendLine();
   builder.AppendFormat("Current Orientation: {0}", DisplayProperties.CurrentOrientation);
   builder.AppendLine();
   builder.AppendFormat("Native Orientation: {0}", DisplayProperties.NativeOrientation);
   builder.AppendLine();
   builder.AppendFormat("Logical DPI: {0}", DisplayProperties.LogicalDpi);
   builder.AppendLine();
   builder.AppendFormat("Resolution Scale: {0}", DisplayProperties.ResolutionScale);
   builder.AppendLine();
   builder.AppendFormat("Is Stereo Enabled: {0}", DisplayProperties.StereoEnabled);
   builder.AppendLine();
   builder.AppendFormat("Supports Keyboard: {0}", IsKeyboardPresent());
   builder.AppendLine();
   builder.AppendFormat("Supports Mouse: {0}", IsMousePresent());
   builder.AppendLine();
   builder.AppendFormat("Supports Touch (contacts): {0} ({1})", IsTouchPresent(), new TouchCapabilities().Contacts);
   builder.AppendLine();
   builder.AppendFormat("Is Network Available: {0}", NetworkInterface.GetIsNetworkAvailable());
   builder.AppendLine();
   builder.AppendFormat("Is Internet Connection Available: {0}", NetworkInformation.GetInternetConnectionProfile() != null);
   builder.AppendLine();
   builder.AppendFormat("Network Host Names: ");
   foreach (var hostName in NetworkInformation.GetHostNames())
   {
      builder.AppendFormat("{0} ({1}), ", hostName.DisplayName, hostName.Type);
   }
   builder.AppendLine();
   builder.AppendFormat("Current Memory Usage: {0:f3} MB", GC.GetTotalMemory(false) / 1024f / 1024f);
   builder.AppendLine();
   builder.AppendFormat("App Temp  Folder: {0}", ApplicationData.Current.TemporaryFolder.Path);
   builder.AppendLine();
   builder.AppendFormat("App Local Folder: {0}", ApplicationData.Current.LocalFolder.Path);
   builder.AppendLine();
   builder.AppendFormat("App Roam  Folder: {0}", ApplicationData.Current.RoamingFolder.Path);
   builder.AppendLine();
   builder.AppendLine();

   if (shouldDumpCompleteDeviceInfos)
   {
      var devInfos = await DeviceInformation.FindAllAsync();
      builder.AppendLine();
      builder.AppendLine("Complete Device Infos:");
      foreach (var devInfo in devInfos)
      {
         builder.AppendFormat("Name: {0} Id: {1} - Properties: ", devInfo.Name, devInfo.Id);
         foreach (var pair in devInfo.Properties)
         {
            builder.AppendFormat("{0} = {1}, ", pair.Key, pair.Value);
         }
         builder.AppendLine();
      }
   }

   return builder.ToString();
}

public static bool IsTouchPresent()
{
 return new TouchCapabilities().TouchPresent == 1;
}

public static bool IsMousePresent()
{
 return new MouseCapabilities().MousePresent == 1;
}

public static bool IsKeyboardPresent()
{
 return new KeyboardCapabilities().KeyboardPresent == 1;
}


On my system it provides the following info (anonymized):
***** System Infos *****

DEBUG

Time: Sun, 28 Oct 2012 07:51:11 GMT
App Name: MyApp
App Version: 1.0.0.5
App Publisher: CN=A8E2F0B1-4749-48AE-AE18-C7FFB5B30272
Supported Package Architecture: Neutral
Installed Location: D:\Development\MyApp\bin\Debug\AppX
Store App Id: 00000000-0000-0000-0000-000000000000
Culture: en-US
OS: WINDOWS
System Manufacturer: Dell Inc.
System Product Name: Precision M6600
System Sku: 
System Name: MYLAPTOP
System ID: 18a2e8c6-cc7d-8546-1e9e-56a12ffb32fd
Hardware ID: 03-00-D2-41-03-00-AA-72-08-00-EF-AD-05-AA-B6-4F-06-00-01-01-04-00-31-1B-04-00-0C-48-04-00-D2-55-04-00-D3-66-01-00-0E-D6-02-00-E4-7F-09-00-56-47
User Display Name: Rene Schulte
Window Bounds w x h: 1920 x 1080
Current Orientation: Landscape
Native Orientation: Landscape
Logical DPI: 96
Resolution Scale: Scale100Percent
Is Stereo Enabled: False
Supports Keyboard: True
Supports Mouse: True
Supports Touch (contacts): False (0)
Is Network Available: True
Is Internet Connection Available: True
Network Host Names: mylaptop (DomainName), mylaptop.local (DomainName), 169.254.85.85 (Ipv4), 192.168.2.148 (Ipv4), 
Current Memory Usage: 0.451 MB
App Temp  Folder: C:\Users\Rene\AppData\Local\Packages\MyApp\TempState
App Local Folder: C:\Users\Rene\AppData\Local\Packages\MyApp\LocalState
App Roam  Folder: C:\Users\Rene\AppData\Local\Packages\MyApp\RoamingState

If you know more system information Windows 8 APIs that listing is missing, please add a comment.

Wednesday, August 29, 2012

Update WriteableBitmapEx for WinRT RTM, WPF, Windows Phone and Silverlight


The RTM version of Windows 8 is now available and with that WriteableBitmapEx was updated too. WriteableBitmapEx is now available for 4 platforms: WPF, Silverlight, Silverlight for Windows Phone and Windows Store Style WinRT .NET.
You can download the binaries here or via the NuGet packageThe packages contain the WriteableBitmapEx binaries. As usual all samples and the source code can be found in the repository.



Since the last WinRT release preview version, a couple of bugs were fixed and a new FromStrean method was added which loads an image stream into a WriteableBitmap. The project was also updated for the Windows 8 RTM version. Please read this blog post for more details about the WinRT version.