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