Tuesday, 23 December 2014

Checking entitlements of iOS app bundle using codesign

I had a problem arise today - it became clear that a production iOS app wasn't requesting push notification permission from users. I went through some tagged releases and tested locally with Xcode, repeatedly installing and uninstalling and changing the date on my phone, and every time I was correctly asked for push permissions when expected.

So my attention turned to the production app and its permissions/entitlements. There was clearly nothing wrong with the code, but perhaps it hadn't been signed properly?

In Xcode's Organizer there is no way (as far as I can tell) to recover the ipa file that was submitted to Apple using the 'Submit' button, so instead I went to the App Store on my mac and downloaded the live production app. In Finder, I changed the .ipa file to a .zip, and opened up the package contents. In the 'payload' directory is the app file. With this, I checked the entitlements like so:
codesign -d --entitlements :- "Payload/YourApp.app"
The response showed that there were no APS entitlements. I compared it to a known working .app file, and the following entry was missing:
<key>aps-environment</key>
It seems that at some point the app was submitted with the wrong Distribution Profile. If it only it wasn't Christmas holidays at iTuneConnect!

Friday, 19 December 2014

Xamarin Android back button event during ActionMode

When a user presses the back button during an ActionMode, OnBackPressed() doesn't get called, s0 how to do we intercept this event if we need to do something?

Like this:
 public override bool DispatchKeyEvent(KeyEvent keyevent)
        {
            if (_contextualActionBar != null && ItemsInEditMode.Any())
            {
                if (keyevent.KeyCode == KeyEvent.KeyCodeFromString("KEYCODE_BACK") && keyevent.Action == KeyEventActions.Up)
                {
                    DeselectallItems(_listOfItems);
                    KillActionBar();
                    return true;
                }
            }
            return base.DispatchKeyEvent(keyevent);
        }

Thursday, 18 December 2014

Screenshot complete listview in Xamarin Android

Today I needed to turn a complete listview into an image, including the below-the-fold content that was yet to be rendered. I found some Java code on SO and ported it to C#. It works a treat:
public static Bitmap GetWholeListViewItemsToBitmap(ListView listview, HistoryAdapter adapter)
        {

            int itemscount = adapter.Count;
            int allitemsheight = 0;
            var bmps = new List();

            for (int i = 0; i < itemscount; i++)
            {

                View childView = adapter.GetView(i, null, listview);
                childView.Measure(View.MeasureSpec.MakeMeasureSpec(listview.Width, MeasureSpecMode.Exactly),
                        View.MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified));

                childView.Layout(0, 0, childView.MeasuredWidth, childView.MeasuredHeight);
                childView.DrawingCacheEnabled = true;
                childView.BuildDrawingCache();
                bmps.Add(childView.GetDrawingCache(true));
                allitemsheight += childView.MeasuredHeight;
            }

            Bitmap bigbitmap = Bitmap.CreateBitmap(listview.MeasuredWidth, allitemsheight, Bitmap.Config.Argb8888);
            Canvas bigcanvas = new Canvas(bigbitmap);

            Paint paint = new Paint();
            int iHeight = 0;

            for (int i = 0; i < bmps.Count; i++)
            {
                Bitmap bmp = bmps[i];
                bigcanvas.DrawBitmap(bmp, 0, iHeight, paint);
                iHeight += bmp.Height;

                bmp.Recycle();
            }

            return bigbitmap;
        }

Tuesday, 16 December 2014

Intent.FLAG_ACTIVITY_NEW_TASK in Xamarin C# Android

While trying to StartActivity from a list adapter I got this warning:
Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
The way to overcome this in Java:
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
The not-so-well-documented way to acheive this using Xamarin:
intent.SetFlags(ActivityFlags.NewTask);

Tuesday, 9 December 2014

Converting from dips to pixels

Useful bit of C# Xamarin code for converting from dips to pixels:
            var dpValue = 10; // value in dips
            var d = AppContext.Resources.DisplayMetrics.Density;
            var pixValue = (int)(dpValue * d); // value in pixels

Wednesday, 12 November 2014

Testing with ExactTarget MobilePush - a couple of gotchas

It was with some reluctance I had to re-enter the world of ExactTarget's MobilePush today, something I've previously had a very poor experience with. This time round was initially met with some frustration (things not doing what you expect them to do) but seeking support was actually a pleasant experience, getting concise accurate answers to my queries within minutes.

The support guy helpfully alerted me to a gotcha, and I picked another one up myself. These together cost me a lot of time:
  1. In your development environment the ET device ID for your device changes regularly. This means that each time you deploy to your device you should check the ET ID, so that when you're sending push messages from the Interactive Marketing Hub you know you have the correct device ID in your audience list. The easiest way to check the ET ID is thus: 
  2. //iOS
    NSLog(@"== DEVICE ID ==\nThe ExactTarget Device ID is: %@\n", [ETPush safeDeviceIdentifier]);
    
    //Android
    Log.v("ETPUSH", new DeviceData().uniqueDeviceIdentifier(getApplicationContext()));
    
  3. The registration database updates on a 15 minute cycle. This means that when your device registers with ET it will take anywhere between 0 and 15 minutes for you to be able to find it in the contacts list.

Once these things were considered, and once I'd fixed my dev app which had mysteriously become unprovisioned (what happened to my APNS certificate that used to be there?) then things looked a lot brighter.

Monday, 10 November 2014

CordovaActivity cannot be resolved to a type

Another problem I had updating my Cordova framework cam with this message:

CordovaActivity cannot be resolved to a type

To resolve this, I went to project Properties -> Java Build Path -> Source and Add Folder, to include the CordovaLib\src folder.

invalid resource directory name: /Applications/AndroidSDK/ [...] /google-play-services_lib/bin/res/crunch

I just updated to the latest Cordova by doing the following:

npm update -g cordova
cordova platform update android 

Which took my Cordova CLI to v4 and my Cordova for Android to 3.6.4.

 I then had a problem building:

     [aapt] invalid resource directory name: /Applications/AndroidSDK/sdk/extras/google/google_play_services/libproject/google-play-services_lib/bin/res/crunch

 The fix turned out to be simple; I deleted the crunch directory and then all was good.


Wednesday, 8 October 2014

Charles proxy setup for mobile debugging

Charles is the defacto web debugging proxy for the Mac, a bit like Fiddler for windows. To really understand what your app is doing while you're building it you should configure your phone to use Charles as a proxy.

To do this on Android you have to select your current network, long press it and choose 'modify'. There you can enter the IP address of your Mac and the default port number 8888. From there it should just work!

For iPhone the task is much the same. Go to the Settings app, tap Wi-Fi, find the network you are connected to and then tap the blue disclosure arrow to configure the network. Scroll down to the HTTP Proxy setting, tap Manual. Enter the IP address of your computer running Charles in the Server field, and the port Charles is running on in the Port field (usually 8888). Leave Authentication set to Off.

Wednesday, 24 September 2014

Xamarin Android touch event handler return false

Sometimes we need to step into a touch event to ascertain something about the touch (typically its location) before other code responds to it.

When this is done we need to remember to sign off with 'e.Handled = false' so that the touch event isn't consumed and can be handled elsewhere. This is the equivalent of the java 'return false'.

      void _myView_Touch(object sender, View.TouchEventArgs e)
        {
            //do your stuff here

            e.Handled = false;

        }

Monday, 22 September 2014

Google Cloud Messaging registration Id changes

This is simply a bookmark entry, reminding myself that if I ever need to wonder about GCM registration id changes (or rather the lack of them, ever) then I should re-visit this excellent SO thread:


Once again, thanks SO.

Monday, 15 September 2014

Android toast positioning

Positioning your android toast using Xamarin C#:
      var toast = Toast.MakeText(appContext, text, ToastLength.Long);
      toast.SetGravity(GravityFlags.Center, 0, 0);
      toast.Show();
You can experiment with different gravity settings, and the following integers nudge the toast in the X or Y direction.

Tuesday, 9 September 2014

Simple android image cropper using Xamarin

I was recently tasked with creating a simple image cropper for an Android app we're building using Xamarin. By 'simple' I mean that the resulting image is always of a fixed aspect ratio - in this case, square.

We start off with an imageview:

<imageview nbsp="" p="">android:background="#fff"
android:id="@+id/photoHolder"
android:layout_gravity="center_horizontal"
android:scaletype="matrix"
android:src="@drawable/TapToAddPhoto_Placeholder">
</imageview>

In our activity we handle the touch event like this:


     
 /// handles touch event so we can pick up on drags/zooms etc
     public override bool OnTouchEvent(MotionEvent theEvent)
        {
            if (_hasProvidedPhoto)
            {
                // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags
                switch (theEvent.Action & MotionEventActions.Mask)
                {
                    case MotionEventActions.Down:
                        _savedMatrix.Set(_matrix);
                        _start.Set(theEvent.GetX(), theEvent.GetY());
                        _mode = Drag;
                        break;
                    case MotionEventActions.PointerDown:

                        _oldDist = Spacing(theEvent);

                        if (_oldDist > 10f)
                        {
                            _savedMatrix.Set(_matrix);
                            MidPoint(_mid, theEvent);
                            _mode = Zoom;
                        }
                        break;
                    case MotionEventActions.Up:
                    case MotionEventActions.PointerUp:
                        _mode = None;
                        break;
                    case MotionEventActions.Move:

                        if (_mode == Drag)
                        {
                            _matrix.Set(_savedMatrix);
                            _matrix.PostTranslate(theEvent.GetX() - _start.X, theEvent.GetY()
                                                                              - _start.Y);
                        }
                        else if (_mode == Zoom)
                        {
                            float newDist = Spacing(theEvent);

                            if (newDist > 10f)
                            {
                                _matrix.Set(_savedMatrix);
                                float scale = newDist / _oldDist;
                                _matrix.PostScale(scale, scale, _mid.X, _mid.Y);
                            }
                        }
                        break;
                }

                _photoHolder.ImageMatrix = _matrix;
            }
            return true;
        }

     //Determine the space between the first two fingers
        private static float Spacing(MotionEvent theEvent)
        {
            float x = theEvent.GetX(0) - theEvent.GetX(1);
            float y = theEvent.GetY(0) - theEvent.GetY(1);
            return FloatMath.Sqrt(x * x + y * y);
        }

     //Calculate the mid point of the first two fingers
        private static void MidPoint(PointF point, MotionEvent theEvent)
        {
            float x = theEvent.GetX(0) + theEvent.GetX(1);
            float y = theEvent.GetY(0) + theEvent.GetY(1);
            point.Set(x / 2, y / 2);
        }

Where _photoHolder is our imageview declared in OnCreate with _photoHolder = FindViewById(Resource.Id.photoHolder).

Then, when the user is happy with their crop, they click a button that creates a bitmap of it with this code:
  
var bitmap = Bitmap.CreateBitmap(v.Width, v.Height, Bitmap.Config.Argb8888);
var canvas = new Canvas(bitmap);
v.Draw(canvas);
return bitmap;
Where v is the ImageView.

This is basically the guts of it. The whole journey of selecting an existing image from the gallery or camera, processing it and dropping it into the image view in the first place proved more tricky than the cropping part.

Here we ask the user if they want to use gallery or camera:

 void addPhotoButton_Click(object sender, EventArgs e)
        {
            string[] options = { "Choose from library", "Take a photo" };

            var builder = new AlertDialog.Builder(this);
            builder.SetCancelable(true);
            builder.SetPositiveButton("Cancel", delegate
            {
                //nothing. just close
            });
            builder.SetItems(options, PhotoChoiceDialogSelected);
            builder.Show();
        }
And here we handle the choice:
 private void PhotoChoiceDialogSelected(object sender, DialogClickEventArgs e)
        {
            if (e.Which == 0) //choose from library
            {
                GetPhotoFromLibrary();
            }
            else //take photo
            {
                GetPhotoFromCamera();
            }
        }
Here are our methods for camera or gallery options:
 private void GetPhotoFromLibrary()
        {
            var imageIntent = new Intent();
            imageIntent.SetType("image/*");
            imageIntent.SetAction(Intent.ActionGetContent);
            StartActivityForResult(
                Intent.CreateChooser(imageIntent, "Select photo"), 0);
        }


        private void GetPhotoFromCamera()
        {
            if (IsThereAnAppToTakePictures())
            {
                CreateDirectoryForPictures();

                var intent = new Intent(MediaStore.ActionImageCapture);
                CameraFile = new File(CameraDir, String.Format("myPhoto_{0}.jpg", Guid.NewGuid()));
                intent.PutExtra(MediaStore.ExtraOutput, Android.Net.Uri.FromFile(CameraFile));
                StartActivityForResult(intent, 1);
            }
            else
            {
                Toast.MakeText(this, "You do not have a camera", ToastLength.Short).Show();
            }
        }

private void CreateDirectoryForPictures()
        {
            CameraDir = new File(Environment.GetExternalStoragePublicDirectory(Environment.DirectoryPictures), "My App Pics");
            if (!CameraDir.Exists())
            {
                CameraDir.Mkdirs();
            }
        }

 private bool IsThereAnAppToTakePictures()
        {
            var intent = new Intent(MediaStore.ActionImageCapture);
            IList availableActivities = PackageManager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
            return availableActivities != null && availableActivities.Count > 0;
        }
And this is what runs when the camera or gallery activity has finished:
 protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
        {
            base.OnActivityResult(requestCode, resultCode, data);

            if (resultCode == Result.Ok)
            {

                var camera = false;

                if (requestCode == 1)
                {
                    camera = true;

                    // make camera image available in the gallery
                    var mediaScanIntent = new Intent(Intent.ActionMediaScannerScanFile);
                    _babyImageUri = Android.Net.Uri.FromFile(CameraFile);
                    mediaScanIntent.SetData(_babyImageUri);
                    SendBroadcast(mediaScanIntent);

                }

                _filePath = camera ? CameraFile.AbsolutePath : GetPathToGalleryImage(data.Data);
                _fileUri = camera ? _babyImageUri : data.Data;

                if (_filePath != null)
                {
                    var scaledBitmap = DecodeSampledBitmapFromResource(
                        _filePath, 575, 575);
                    if (scaledBitmap != null)
                    {
                        var matrix1 = new Matrix();
                        matrix1.PostRotate(GetCorrectOrientation(_filePath));
                        var rotatedBitmap = Bitmap.CreateBitmap(scaledBitmap, 0, 0, scaledBitmap.Width,
                            scaledBitmap.Height,
                            matrix1, true);
                        _photoHolder.SetImageBitmap(rotatedBitmap);
                        _photoHolder.LayoutParameters.Height = _photoHolder.Width;
                       
                    }
                }

            }
        }
And these are some important supporting methods used in the above code:
 /// 
        /// takes a uri to a gallery image and returns a path
        /// 
        /// 
        /// 
        private string GetPathToGalleryImage(Android.Net.Uri uri)
        {
            if (uri != null)
            {
                string path = null;
                // The projection contains the columns we want to return in our query.
                var projection = new[] { MediaStore.Images.Media.InterfaceConsts.Data };
                using (ICursor cursor = ManagedQuery(uri, projection, null, null, null))
                {
                    if (cursor != null)
                    {
                        int columnIndex = cursor.GetColumnIndexOrThrow(MediaStore.Images.Media.InterfaceConsts.Data);
                        cursor.MoveToFirst();
                        path = cursor.GetString(columnIndex);
                    }
                }
                return path;
            }
            return null;
        }


        /// 
        /// google recommended code for displaying large bitmaps
        /// 
        /// 
        /// 
        /// 
        /// 
        public Bitmap DecodeSampledBitmapFromResource(String path,
            int reqWidth, int reqHeight)
        {
            // First decode with inJustDecodeBounds=true to check dimensions
            var options = new BitmapFactory.Options
            {
                InJustDecodeBounds = true,
                InPreferredConfig = Bitmap.Config.Argb8888
            };
            BitmapFactory.DecodeFile(path, options);

            // Calculate inSampleSize
            options.InSampleSize = CalculateInSampleSize(options, reqWidth,
                    reqHeight);

            // Decode bitmap with inSampleSize set
            options.InJustDecodeBounds = false;
            return BitmapFactory.DecodeFile(path, options);
        }

        /// 
        /// google recommended code for displaying large bitmaps
        /// 
        /// 
        /// 
        /// 
        /// 
        public int CalculateInSampleSize(BitmapFactory.Options options,
           int reqWidth, int reqHeight)
        {
            // Raw height and width of image
            int height = options.OutHeight;
            int width = options.OutWidth;
            int inSampleSize = 1;

            if (height > reqHeight || width > reqWidth)
            {

                int halfHeight = height / 2;
                int halfWidth = width / 2;

                // Calculate the largest inSampleSize value that is a power of 2
                // and keeps both
                // height and width larger than the requested height and width.
                while ((halfHeight / inSampleSize) > reqHeight
                        && (halfWidth / inSampleSize) > reqWidth)
                {
                    inSampleSize *= 2;
                }
            }

            return inSampleSize;
        }


        private float GetCorrectOrientation(String filename)
        {

            ExifInterface exif = new ExifInterface(filename);
            int orientation = exif.GetAttributeInt(ExifInterface.TagOrientation, 1);

            switch (orientation)
            {
                case 6:
                    return 90;
                case 3:
                    return 180;
                case 8:
                    return 270;
                default:
                    return 0;
            }
        }

Friday, 27 June 2014

ExactTarget - the worst DX ever.

What is DX and why does it matter? DX stands for 'Developer Experience'. It refers to the experience that developers like me have when implementing, or integrating with, technical products or services. It matters because a product owner's failure to assist developers with implementation has some negative side effects.

Firstly, implementation may be slower than desired, with cost and time implications for client. Secondly, developers, particularly freelance or contract developers like myself, will actively discourage future clients from using the product again, and will instead hope that a competing product performs better. Thirdly, the client will also remember the bad DX and will try to avoid the product when next given the choice, perhaps in a new role at a different company. Finally, it will annoy some developers so much they might just blog about it in an attempt to save their kinfolk from a similar fate.

Enter ExactTarget. I was recently tasked with implementing their MobilePush product into an iOS and Android app. The documentation on how to achieve this was sufficient, and I had it up and running in basic guise within a day, managing to register devices for push notification and send messages to them. But then I started having problems. Firstly, I couldn't set up locations because despite my client paying for it it hadn't been enabled on their account. It took me about 1hr of head-scratching to first suspect this, and then to get confirmation of it took ET 14 working hours, and then another 4.5hrs to rectify. In total this is nearly 3 working days. When I was finally given access to locations I couldn't get them to work - my entry and exit message have never been received on either of my opted-in devices. 6 further working days later I am yet to get a satisfactory explanation as to why this is - have I done something wrong with the code? Am I doing something wrong with the messaging GUI? Is there something wrong at their end? Did they forget to switch something on? I have absolutely no idea, because every time I request help it takes hours, sometimes days, to get a response, and when the response comes it hasn't addressed the problem sufficiently, or tells me to do something I've already done. Then I have to respond and wait another Xhrs/days.

ExactTarget's non-support smacks of a company going through growth pains. Their technicians (the people that can really resolve problems) are probably not in the UK, and their Solutions Architects (the 'technical' people that we deal with) are probably unleashed onto customers and developers with rapid (insufficient) training and little actual technical ability/tools/permissions to troubleshoot for developers, leaving them hamstrung by cross-time-zone comms with their probably over-stretched under-resourced technicians.

I have little doubt that there's someone at ExactTarget who could probably understand and diagnose the problems I've been having within the space of a short phonecall. Unfortunately this isn't going to happen as a) they don't do this and b) this is my last day on this contract and I'm having to leave my client with what I believe to be a correct and working implementation but with little evidence that this is the case. I wish them luck getting the outstanding issues resolved.

Monday, 23 June 2014

iOS push notification prompt no longer appearing

Today I was having trouble getting my app to prompt me for push notification preferences even when uninstalling and re-installing. It seems that the OS remembers these preferences (and others) for 24hrs even after deleting an app, so the only way to circumvent this and pretend you're on a completely clean install is this:
  •  Delete your app from the device.
  •  Turn the device off completely and turn it back on.
  •  Go to Settings > General > Date & Time and set the date ahead a day or more.
  •  Turn the device off completely again and turn it back on.
  •  Re-install app

Friday, 23 May 2014

ExactTarget MobilePush setObjectForKey: object cannot be nil (key: app_version)


Recently I've been integrating the ExactTarget MobilePush SDKs into the Android and iOS code running behind my Cordova/Phonegap app. It hasn't been plain sailing due to documentation that isn't too brilliant, slow comms from the SalesForce team, and technical hurdles that are difficult to find answers to.

One problem was a crash when running my iOS app with the newly-integrated ExactTarget MobilePush iOS SDK. I kept getting this error:

Uncaught exception: *** setObjectForKey: object cannot be nil (key: app_version)

At first I thought this may have arisen because my app was yet to be provisioned by ExactTarget, and it was their code causing the error, and perhaps it might fix itself once I had received notification that I had been provisioned. Finally after a couple of days I received this notice, but alas the error persisted.

After more research I discovered that my app didn't have an Info.plist entry for "Bundle versions string, short" (or CFBundleShortVersionString in the source). Once I added this my error disappeared!

Wednesday, 21 May 2014

Retrieve the SHA1 fingerprints of the Android Debug Key

Note to self. How to retrieve the SHA1 fingerprints of the Android Debug Key using a Terminal window:

keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -list -v

And the output looks like this:


Tuesday, 20 May 2014

Git checkout error: Unable to unlink 'plugins/...'

Had a strange git error today when finishing a GitFlow release on my PhoneGap/Cordova app. It was occurring when it seemingly tried to checkout master, and then would fail, leaving a whole heap of dirty unfinished work in my develop branch.

So I hard-reset the dev branch and attempted to checkout master manually. Same error - 'unable to unlink' and then lots of references to a certain geolocation plugin in the /plugins/ dir. Looking in detail at this directory it was clear that it had different ownership to all the other dirs. Everything else was owend by edpitt, but this was owned by 'root', for some unknown reason. So I ran a 'sudo chown -R edpitt /plugins/org.apache.cordova.geolocation' to turn it back to the correct ownership.

Once this was complete I was able to checkout master with no errors, and so attempting a GitFlow release this time proved successful.

Tuesday, 13 May 2014

Android 4.4.2 tap event registering when unhiding element

This is weird. I have two images of a heart, one grey, one red. I use them to allow a user to 'favourite' an item in a list. When the grey heart is tapped I hide it and show the red heart. When the red heart is tapped I hide it and show the grey heart. Simple huh? You can see them in this pic here:



The code looks something like this:
       
      $( ".greyHeart" ).bind( "tap", function( event ){
                    
              $(this.parentNode).addClass('favourited');
              $(this.parentNode).removeClass('notFavourited');
                    
               //store data in array in local storage
           
        });

       $( ".redHeart" ).bind( "tap", function( event ){

              $(this.parentNode).addClass('notFavourited');
              $(this.parentNode).removeClass('favourited');
          
             //remove data from array in local storage

        });

This all works brilliantly across my desktop browsers, iOS and most Androids. Except 4.4.2.

In 4.4.2 it seems that the tap event is detected on the grey heart and then again the tap event is detected on the red heart too when it appears under the user's finger. The user sees the heart turn red then immediately grey again.

To get around this odd behaviour I had to change my code to immediately unbind tap events on detecting a tap, and then rebind them after a small delay.

Friday, 11 April 2014

Google analytics event tracking in PhoneGap / Cordova

I spent (wasted) hours trying to get this GAPlugin to work but simply couldn't. So I went back to square one, did some more googling and found Dan Wilson's Google Analytics Plugin. I'm pleased to say I had it running in minutes simply by following his concise instructions:

cordova plugin add https://github.com/danwilson/google-analytics-plugin.git

Then, in deviceready add the following:

analytics.startTrackerWithId('UA-XXXX-YY') 

And for each event, simply:

analytics.trackEvent('Category', 'Action', 'Label', Value) 

Job done. Big-up to Dan Wilson!

Tuesday, 1 April 2014

Leaflet.js clickable marker label

I couldn't find any documentation or examples on how to make a label clickable in leaflet.js.

I tried adding a myLabel.on('click',function(){ }); event but it simply wasn't working. After poking around in the console for a while and logging the label object I found you need to change a seemingly undocumented 'clickable' option. So I ended up with something like this:


var myLabel = myMarker.label;
myLabel.options.clickable = true;
myLabel.on('click',function(){  
//do your stuff
})

Friday, 28 March 2014

Visual Studio slow startup when debugging

I've been recently struggling with a really slow startup time when debugging in Visual Studio. Typically it would take nearly two minutes, which is unacceptable especially if you're working on something that requires regular iterations and test runs, which is a way I like to work.

I tried symbol caching but this had little effect. Eventually I picked up on a subtle comment on an SO thread where someone had resolved a similar problem simply by deleting a breakpoint.

So, I deleted all my breakpoints, and hey presto my startup time was sorted - back to an acceptable 20 seconds!

Wednesday, 26 February 2014

Markers, popups and labels with leaflet and openstreetmap

It took me a while to get this code working right, and now I have I thought I'd share/store it. It could probably do with a little refactoring by outsourcing some of it to other methods, but it's good to go and its linear nature makes it easier to post here. I started this off using google maps but the performance was terrible and after a bit of zooming/panning the app would crash when running on a device. So I opted for leaflet with openstreetmap and I'm really please with the results.

What I'm doing with this code is plotting 40 or so holiday parks with markers and labels, with an associated popup when clicked. In the popup is a button (a JQM widget) that performs an action. Further to this I wanted to switch the marker for a larger one and hide/show labels at a certain zoom level, and I also wanted to specify different label orientations (top,right,left,bottom) for each park.

The end result looks something like this:

              

mapPagePopulate:function(){

                var self=this;
                self.show_loader("Loading map data...");

                //set initial zoom depending on device
                var initZoom = 7;
                if(self.deviceIsMobile()){ initZoom = 6; }

                //init map with centre around Nottingham
                var map = L.map('map_canvas').setView([52.946104,-1.170044], initZoom);
              
                //add copyright - v important!
                L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                    attribution: 'Map data © OpenStreetMap contributors',
                    maxZoom: 18,
                    minZoom: 6
                }).addTo(map);


                //get park data from local storage
                    var parkListLite = $.parseJSON(window.localStorage.getItem(self.localStorageKeys.parkListLite));
                   
                   //declare empty arrays
                    var locations = [];
                    var markers = [];

                    //get label positiong (t,r,b,l)
                    var labelPositions = self.settings.mapLabelPositions();
   
                    //loop through park data, create location object with everything we need,
                    //and push it to locations array
                    $(parkListLite).each(function(i, obj){ 
                        var location = [];

                        //popup html
                        var calloutMarkup = String.format('

{0}

'+ '{1} '+ 'From {4}', obj['ParkName'], obj['MapCopy'], obj['EPiServerPageId'], self.endpoints.img_url + obj['MapThumbnail'], self.utils.makePriceFriendly(obj['CheapestPrice'],"£") ); var wrapper = document.createElement("div"); wrapper.innerHTML = calloutMarkup; wrapper.className="mapCallout"; location[0] = obj['ParkName']; location[1] = obj['Latitude']; location[2] = obj['Longitude']; location[3] = wrapper; location[4] = labelPositions[obj['ParkName']]; location[5] = ''+ obj['ParkName'] +''; locations.push(location); }); //create icons var mapMarkerSmall, mapMarkerLarge, iconAnchor; $(locations).each(function(i, loc){ //label anchor influences where the popup appears //here we check if we want top, left, right or bottom //and specify the anchor and css accordingly switch(loc[4]) { case 't': labelAnchor= [-128,-63]; labelClass = "mapLabel top"; break; case 'r': labelAnchor= [-263,-24]; labelClass = "mapLabel right"; break; case 'b': labelAnchor= [-128,25]; labelClass = "mapLabel bottom"; break; case 'l': labelAnchor= [0,-24]; labelClass = "mapLabel left"; break; default: labelAnchor= [0,-24]; labelClass = "mapLabel left"; } //create the large and small icon for each park, with the anchor specified above mapMarkerSmall = L.icon({ iconUrl: 'img/mapParkMarker.png', iconRetinaUrl: 'img/mapParkMarker.png', iconSize: [20, 20], iconAnchor: [10,10], popupAnchor: [0,-20], labelAnchor: labelAnchor }); mapMarkerLarge = L.icon({ iconUrl: 'img/mapMarkerLarge.png', iconRetinaUrl: 'img/mapMarkerLarge.png', iconSize: [130, 130], iconAnchor: [65,65], popupAnchor: [0,-20], labelAnchor: labelAnchor }); //add marker to map with label var marker = L.marker([loc[1], loc[2]], {icon: mapMarkerSmall}).bindLabel(loc[5], {noHide: true,direction: 'left',className:labelClass}).addTo(map); //bind popup to marker marker.bindPopup(loc[3]); //we can assign arbitrary properties to marker. here we assign small and large markers marker.iconSmall = mapMarkerSmall; marker.iconLarge = mapMarkerLarge; //hide labels on initial load map.removeLayer(marker.label); //push marker to marker array markers.push(marker); }); //for some zooms we need to show/hide labels or switch out markers for larger/smaller map.on('zoomend', function(event) { var zoom = map.getZoom(); var markerLabelClass; for (i = 0; i < locations.length; i++) { markerLabelClass = markers[i].labelClass; if(zoom <= 10){ markers[i].setIcon(markers[i].iconSmall); map.removeLayer(markers[i].label); }else{ markers[i].setIcon(markers[i].iconLarge); map.addLayer(markers[i].label); } } }); //capture popup open event and run JQM create so it creates the button widget //also assign click event to park view button map.on('popupopen', function(e) { $('#map_canvas').trigger('create'); $('.parkButton').unbind(); $('.parkButton').on('click',function(){ self.setLocalStorage(self.localStorageKeys.lastRequestedParkId,$(this).data('parkPageId')); self.utils.notify('Not yet implemented','OK'); //$.mobile.changePage('#parkoverview'); }); }); self.hide_loader(); }

SkyDrive needs to be updated AppStoreRequiredUpdateAva'ilableMessage

So, it looks like Microsoft have lazily given us a painful upgrade path now that they've been forced to rename SkyDrive. This morning I logged into my Mac and got this bizarre message:



At first I thought this may be some kind of virus duping me into handing over my SkyDrive detail, and there's no mention of this update in the app store, presumably because the SkyDrive app no longer exists, and it's not an update at all, more a replacement.

They've switched off SkyDrive and done a runner; The 'Get the Update' button takes you to the appstore where you can download OneDrive, and sure enough you have to reconfigure your SkyDrive settings (folder, preferences) all over again, just like you were a new user. As I type, my new OneDrive installation is slowly synching with the server. It was all bang up-to-date yesterday at 5pm but it looks like it's going through every single file to work out what yesterday it already knew.

And no doubt I'll have to go through the same ball-ache on my other devices too. Grrr.

Friday, 21 February 2014

Changing the name of a Phonegap project

This is always a pain, but these instructions found on StackOverflow seem to hit the spot:
  • Edit the file ./.cordova/config.json Change the "name" field to your new project name. 
  • Edit the file ./www/config.xml Change the "name" field to your new project name. 
  • Make a copy of your ./platforms/ios directory (optional, only needed if you have modified the ios code directly) 
  • Remove the ./platforms/ios directory. 
  • Run "phonegap build ios" This will create a new project with the correct name.

Thursday, 6 February 2014

Safari text input fields and -webkit-user-select: none;

I've been using this in my css to prevent selection of text with long presses:
/* remove touch callout - visible when pressing and holding a link */
* {
-webkit-touch-callout: none;
-webkit-user-select: none;
}
But I discovered today that in Safari only (including iPhone) this actually prevents the input/edit of text in an text input box. The fix is to override the css for inputs like so:
input {
    -webkit-user-select: auto !important;
}

Thursday, 30 January 2014

Git list files assume-unchaged

git ls-files -v | egrep -r "^h .*"

And to change their status:

 git update-index --no-assume-unchanged myfile.name
 git update-index --assume-unchanged myfile.name