Programming

How to detect a user pan/touch/drag on Android Map v2

If you are impatient, you can skip directly to the solution.

The new implementation of the Android Map (v2) is great in many ways and their support for map fragments is awesome, but they fall a bit short in a couple of minor but very necessary implementations. That makes the maps API a bit incomplete.

I am referring in particular to the ability to distinguish between a user dragging (panning) the map, verses a programmatic move of the map.

That looks like a very basic need that a lot of Android programmers would face when dealing with the new map implementation. I have a number of apps, where I would like to update the map after the user has panned the map to a new location and I want to do that ONLY after they had stopped moving the map. I don’t want to trigger multiple updates while they are in the process of panning.

As you can deduce so far, there is no straight up way to implement this very basic behavior… or at least I could not find one. So, if you have found one, please let me know… I am only human and I might have missed something!

There are a couple of listeners implemented that kind of give you something to work with at a first glance, but they are both incomplete.

We have onCameraChangeListener, which gets called every time the map moves, zooms in/out or the camera angle changes, but it is completely useless in helping us implement the above needed behavior. It does not tell you in any way if the map was moved due to a user interaction or due to some other event triggering the map’s “camera change”. Furthermore, if you implement this listener you will notice that it gets triggered multiple times during a map panning. So, this makes it completely inefficient, especially when you are fetching data over the network all the time. The implementation of this listener could have been a bit more complete, if the only added a boolean value to the GoogleMap.OnCameraChangeListener indicating if the action was done by a human or not. Just like they implemented the SeekBar.OnSeekBarChangeListener.

Furthermore, you might think that the this situation can be remedied with the map’s setOnMapClickListener, but you would be wrong, as you cannot use it in any way to distinguish between a touch down and a touch up events.

That finally leads us down the path of implementing this basic map behavioral need ourselves! I would like to think that Google does this to us, because they know we are better programmers than those stinking iOS dudes, who are used to getting anything they need handed on a platter of APIs:-) But may be I am wrong again…

So, a basic search led me to a couple of stackoverflow posts that try to tackle this issue:

How to handle onTouch event for map in Google Map API v2?
Google maps android api v2 – detect touch on map

But that sill did not completely solve my problem due to a couple of reasons. One is that they use an ugly static boolean variable to comunicate back to the map activity if the map was touched or not. Further more they use the that boolean in the setOnCameraChangeListener to decide whether to update the map or not, which flat out does not work. And the reason it does not work is because in most of the cases the setOnCameraChangeListener listener is triggered before the boolean value was updated, resulting in a failure of the code after that to execute.

So I took all this and made a few small but significant changes, namely using a custom interface to trigger the map to update at the right time. The TouchableWrapper class declares a UpdateMapAfterUserInterection interface, which is implemented by the Map Activity. That gives us a very fine control over the map events and what we want to do with them. Now a complete disclaimer… This all works very well, but it is a hacky way of doing things. Extending the map fragment, then putting a frame layout on top of if to intercept user interaction and then passing it over to the map activity just feels (and is) hacky. But it is the best solution I could find or think of for now. Please share if you have a better solution!

Here is the complete implementation:

UpdateMapAfterUserInterection class


public  class TouchableWrapper extends FrameLayout {

	private long lastTouched = 0;
	private static final long SCROLL_TIME = 200L; // 200 Milliseconds, but you can adjust that to your liking
	private UpdateMapAfterUserInterection updateMapAfterUserInterection;

	public TouchableWrapper(Context context) {
		super(context);
		// Force the host activity to implement the UpdateMapAfterUserInterection Interface
		try {
			updateMapAfterUserInterection = (ActivityMapv2) context;
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString() + " must implement UpdateMapAfterUserInterection");
        }
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		switch (ev.getAction()) {
		case MotionEvent.ACTION_DOWN:
			lastTouched = SystemClock.uptimeMillis();
			break;
		case MotionEvent.ACTION_UP:
			final long now = SystemClock.uptimeMillis();
			if (now - lastTouched > SCROLL_TIME) {
				// Update the map
				updateMapAfterUserInterection.onUpdateMapAfterUserInterection();
			}
			break;
		}
		return super.dispatchTouchEvent(ev);
	}

	// Map Activity must implement this interface
    public interface UpdateMapAfterUserInterection {
        public void onUpdateMapAfterUserInterection();
    }
}

MySupportMapFragment class


public class MySupportMapFragment extends SupportMapFragment{
	public View mOriginalContentView;
	public TouchableWrapper mTouchView;

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
		mOriginalContentView = super.onCreateView(inflater, parent, savedInstanceState);
		mTouchView = new TouchableWrapper(getActivity());
		mTouchView.addView(mOriginalContentView);
		return mTouchView;
	}

	@Override
	public View getView() {
		return mOriginalContentView;
	}
}

The layout for the map activity


<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mapFragment"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_below="@+id/buttonBar"
    class="com.myFactory.myApp.MySupportMapFragment"
    />

And finally the Map Activity


public class ActivityMapv2 extends FragmentActivity implements UpdateMapAfterUserInterection {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }

// Implement the interface method
public void onUpdateMapAfterUserInterection() {
		// TODO Update the map now
        }
 }
Tuesday, April 9th, 2013 Android, Eclipse, Java, Programming 8 Comments

Implementing Android GCM in a different package other than the main application package

There is something you need to be aware of if you are placing your GCM (Google Cloud Messaging, aka push notifications) service (the one extending from GCMBaseIntentService) in a package other than the main application package. The default implementation of the GCMBroadcastReceiver class, whcih is part of the GCM library assumes that your service is going to be in the main application package. If the service is in a different package though, then it will not be able to be started by the broadcast receiver and if you are debugging your app it will look like your app requests a Registration ID from the Google servers, but then everything dies off and you are not getting a response back.

What you need to do in this case is extend the GCMBroadcastReceiver class and override the getGCMIntentServiceClassName method to return the package name of the service and the service class name. So for example, if your service was called GCMIntentService in package com.mycompany.mainpackage.somepackage, you would return: “com.mycompany.mainpackage.somepackage.GCMIntentService”. Here is an example of the extanded GCMBroadcastReceiver class:


public class GCMMyBoadcastReceiver extends GCMBroadcastReceiver {
	protected String getGCMIntentServiceClassName(Context contest) {
		return "com.mycompany.mainpackage.somepackage.GCMIntentService";
	}
}

Also, do not forget to update your manifest file to make sure that the new extended broadcast receiver is used:


<receiver android:name="com.mycompany.mainpackage.somepackage.GCMMyBoadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND">
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
	<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
        <category android:name="com.mycompany.mainpackage" />
   </intent-filter>
</receiver>

This little detail wasted a coule of hours of my time trying to figure out what I did wrong. Unfortunatly this detial is not well documented (not even mentioned) in their GCM implementation tutorial.

Monday, November 26th, 2012 Android, Eclipse, Java, Programming 1 Comment

Quick and easy way to remove all single line (//) comments in Eclipse

I had to quickly remove single line comments from an Android project in Eclipse. I could not find any built in feature in Eclipse to do that. So I resorted to a simple regular expression:

(//[^\n]*)

This simple regex selects all the strings that begin with “//” and end with a new line. So all you have to do is use it in the find and replace option for a file or to apply to the whole project or multiple files do Search -> File. Also, make sure to select the “regular expression” check box.

This is something I did very quickly… so be careful since it might pick up some urls as well… like anything after the “//” part in “http://mysite.com/page.php”.

Wednesday, November 7th, 2012 Android, C++, Eclipse, Java, Programming No Comments

A small bug in the Android In-App Billing demo app causes “Item Not Found”

Important: This is not a bug in the API! It is only a bug in the demo Dungeons application that Google provides.

Several months ago (May, 2012) Google released the new version (v2) of the Android In-App Billing API that supports auto-renewing subscriptions. That was a huge step forward! Thank you Google!
I started implementing this in my apps soon after but then discovered one small annoying bug in their demo application (Dungeons). It causes the the incorrect response of “item not found”.
I published the solution a couple of months ago to stackoverflow and the official android developers forum.

The bug is in the onClick for the Purchase button in the Dungeons class of the sample application.

The supplied method has a bug in the if {} else if {} statement where it causes the mBillingService.requestPurchase to be called twice, when the selected item is not a subscription item (mManagedType != Managed.SUBSCRIPTION). So the same item will be requested twice, once with an item type of “inapp” (which is the valid request) and immediately after that with an item type of “subs” (which is incorrect and it shows “item not found”).

Here is the buggy code:

if (mManagedType != Managed.SUBSCRIPTION &&
                    !mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_INAPP, mPayloadContents)) {
                showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
            } else if (!mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_SUBSCRIPTION, mPayloadContents)) {
                // Note: mManagedType == Managed.SUBSCRIPTION
                showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
}

To fix this, add mManagedType == Managed.SUBSCRIPTION to the else if above.

Here is how the function should look:

public void onClick(View v) {
        if (v == mBuyButton) {
            if (Consts.DEBUG) {
                Log.d(TAG, "buying: " + mItemName + " sku: " + mSku);
            }

            if (mManagedType != Managed.SUBSCRIPTION &&
                    !mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_INAPP, mPayloadContents)) {
                showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
            } else if (mManagedType == Managed.SUBSCRIPTION && !mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_SUBSCRIPTION, mPayloadContents)) {
                // Note: mManagedType == Managed.SUBSCRIPTION
                showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
            }
        } else if (v == mEditPayloadButton) {
            showPayloadEditDialog();
        } else if (v == mEditSubscriptionsButton) {
            editSubscriptions();
        }
    }
Wednesday, August 22nd, 2012 Android, Eclipse, General, Java, Programming No Comments

How to get Picasa images using the Image Picker on Android devices running any OS version

Using the Image Picker to get a local image from the Gallery is pretty easy and trivial task. But things get a lot more interesting if the user has a Picasa account and he/she happens to select an image from one of their Picasa albums. If you do not handle this scenario, your app will crash! And there is no way for you to tell the Image Picker to show just local files. So, you have to handle it, or you will be releasing a buggy application!

Things got even more interesting after the release of Honeycomb! All of a sudden the code that was fetching Picasa images and was working flawlessly started failing on devices running OS 3.0 and up. After some investigation I found the culprit- Google changed the URI returned when the user was selecting a Picasa image. This change was completely undocumented, or at least I could not find any documentation on this! So, on devices running Android OS prior to 3.0 the URI returned was an actual URL and now on devices running OS 3.0 and higher, the URI returned had a different format. For example:

1. https://lh4.googleusercontent.com/… (URI returned on devices running OS prior to 3.0)
2. content://com.google.android.gallery3d (URI returned on devices running OS 3.0 and higher)

I posted a brief solution on the official Android bug report site, but I will expand on it here.

In order to properly handle fetching an image from the Gallery you need to handle three scenarios:

1. The user selected a local image file
2. The user selected a Picasa image and the device is running Android version prior to 3.0
3. The user selected a Picasa image and the device is running Android version 3.0 and higher

Let’s delve straight into the code:

1. Start the Image Picker:


private static final int PICTURE_GALLERY = 1;
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(photoPickerIntent, PICTURE_GALLERY);

In the first line above I am passing a URI to the intent because I would like to fetch the full image, not just get a Bitmap with the thumbnail. The Android team did a good job of acknowledging that if you just wanted a thumbnail of the image then you can get back a Bitmap object containing it, since a thumbnail would not be that big in size. But if you wanted the full image, it would be foolish to do the same, since that would take up a big chunk of the heap! So, what you get instead is some data about the image file, among which is the path to it.

2. We need to implement the onActivityResult method that will be called when the user closes (picks the image) the Image Gallery application:


@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
	super.onActivityResult(requestCode, resultCode, intent);
	switch (requestCode) {
		case PICTURE_GALLERY:
		if (resultCode == RESULT_OK && intent != null) {
			Uri selectedImage = intent.getData();
			final String[] filePathColumn = { MediaColumns.DATA, MediaColumns.DISPLAY_NAME };
			Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null);
			// some devices (OS versions return an URI of com.android instead of com.google.android
			if (selectedImage.toString().startsWith("content://com.android.gallery3d.provider"))  {
				// use the com.google provider, not the com.android provider.
				selectedImage = Uri.parse(selectedImage.toString().replace("com.android.gallery3d","com.google.android.gallery3d"));
			}
			if (cursor != null) {
				cursor.moveToFirst();
				int columnIndex = cursor.getColumnIndex(MediaColumns.DATA);
				// if it is a picasa image on newer devices with OS 3.0 and up
				if (selectedImage.toString().startsWith("content://com.google.android.gallery3d")){
					columnIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME);
					if (columnIndex != -1) {
						progress_bar.setVisibility(View.VISIBLE);
						final Uri uriurl = selectedImage;
						// Do this in a background thread, since we are fetching a large image from the web
						new Thread(new Runnable() {
							public void run() {
								Bitmap the_image = getBitmap("image_file_name.jpg", uriurl);
							}
						}).start();
					}
				} else { // it is a regular local image file
					String filePath = cursor.getString(columnIndex);
					cursor.close();
					Bitmap the_image = decodeFile(new File(filePath));
				}
			}
			// If it is a picasa image on devices running OS prior to 3.0
			else if (selectedImage != null && selectedImage.toString().length() > 0) {
				progress_bar.setVisibility(View.VISIBLE);
				final Uri uriurl = selectedImage;
				// Do this in a background thread, since we are fetching a large image from the web
				new Thread(new Runnable() {
					public void run() {
						Bitmap the_image = getBitmap("image_file_name.jpg", uriurl);
					}
				}).start();
			}
		}
		break;
	}
}

3. Implement the getBitmap method, which will download the image from the web if the user selected a Picasa image:


private Bitmap getBitmap(String tag, Uri url)
{
	File cacheDir;
	// if the device has an SD card
	if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) {
		cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),".OCFL311");
	} else {
		// it does not have an SD card
	   	cacheDir=ActivityPicture.this.getCacheDir();
	}
	if(!cacheDir.exists())
	    	cacheDir.mkdirs();

	File f=new File(cacheDir, tag);

	try {
		Bitmap bitmap=null;
		InputStream is = null;
		if (url.toString().startsWith("content://com.google.android.gallery3d")) {
			is=getContentResolver().openInputStream(url);
		} else {
			is=new URL(url.toString()).openStream();
		}
		OutputStream os = new FileOutputStream(f);
		Utils.CopyStream(is, os);
		os.close();
		return decodeFile(f);
	} catch (Exception ex) {
		Log.d(Utils.DEBUG_TAG, "Exception: " + ex.getMessage());
		// something went wrong
		ex.printStackTrace();
		return null;
	}
}

The decodeFile method takes a reference to a file and returns a Bitmap. This is pretty trivial. In my case I also scale down the image as I am reading it from the file, so it does not take much memory.

I think Google has some work to do about all this and if they want to seamlessly integrate Picasa with the Android Gallery application, they should have done a better job with the Image Picker functionality. The developer should not be doing all this heavy lifting and be concerned with where the actual image resides. The returned URI should be exactly the same no matter if the image is local or not. And if it is not local, the Gallery app should be fetching it for us. That way, we will have a consistent and predictable functionality. Right now you will be surprised how many applications out there do not handle Picasa images and crash with an ugly error.

Thursday, March 1st, 2012 Android, Eclipse 11 Comments

Search

 

Archive

May 2013
M T W T F S S
« Apr    
 12345
6789101112
13141516171819
20212223242526
2728293031  

Other