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

Software Center in Ubuntu 12.04 crashes on startup

Recently the Software Center started crashing on startup. Initially I thought it was due to network issues, but after a further examination it turned out not to be the case.
Running it from the panel will not give you any feedback about why it crashes, so I executed it command line to see what is going on:

/usr/bin/software-center

That produced the following output:

2012-05-23 09:47:04,729 - softwarecenter.ui.gtk3.app - INFO - setting up proxy 'None'
2012-05-23 09:47:04,732 - softwarecenter.db.database - INFO - open() database: path=None use_axi=True use_agent=True
2012-05-23 09:47:04,926 - softwarecenter.backend.reviews - WARNING - Could not get usefulness from server, no username in config file
2012-05-23 09:47:05,129 - softwarecenter.db.pkginfo_impl.aptcache - INFO - aptcache.open()
Traceback (most recent call last):
  File "/usr/share/software-center/softwarecenter/db/pkginfo_impl/aptcache.py", line 243, in open
    self._cache = apt.Cache(GtkMainIterationProgress())
  File "/usr/lib/python2.7/dist-packages/apt/cache.py", line 102, in __init__
    self.open(progress)
  File "/usr/lib/python2.7/dist-packages/apt/cache.py", line 145, in open
    self._cache = apt_pkg.Cache(progress)
SystemError: E:Encountered a section with no Package: header, E:Problem with MergeList /var/lib/apt/lists/us.archive.ubuntu.com_ubuntu_dists_precise_multiverse_binary-amd64_Packages, E:The package lists or status file could not be parsed or opened.
2012-05-23 09:47:06,770 - softwarecenter.db.enquire - ERROR - _get_estimate_nr_apps_and_nr_pkgs failed
Traceback (most recent call last):
  File "/usr/share/software-center/softwarecenter/db/enquire.py", line 115, in _get_estimate_nr_apps_and_nr_pkgs
    tmp_matches = enquire.get_mset(0, len(self.db), None, xfilter)
  File "/usr/share/software-center/softwarecenter/db/appfilter.py", line 89, in __call__
    if (not pkgname in self.cache and
  File "/usr/share/software-center/softwarecenter/db/pkginfo_impl/aptcache.py", line 263, in __contains__
    return self._cache.__contains__(k)
AttributeError: 'NoneType' object has no attribute '__contains__'
Traceback (most recent call last):
  File "./software-center", line 176, in 
    app.run(args)
  File "/usr/share/software-center/softwarecenter/ui/gtk3/app.py", line 1343, in run
    self.show_available_packages(args)
  File "/usr/share/software-center/softwarecenter/ui/gtk3/app.py", line 1273, in show_available_packages
    self.view_manager.set_active_view(ViewPages.AVAILABLE)
  File "/usr/share/software-center/softwarecenter/ui/gtk3/session/viewmanager.py", line 149, in set_active_view
    view_widget.init_view()
  File "/usr/share/software-center/softwarecenter/ui/gtk3/panes/availablepane.py", line 168, in init_view
    self.apps_filter)
  File "/usr/share/software-center/softwarecenter/ui/gtk3/views/catview_gtk.py", line 240, in __init__
    self.build(desktopdir)
  File "/usr/share/software-center/softwarecenter/ui/gtk3/views/catview_gtk.py", line 491, in build
    self._build_homepage_view()
  File "/usr/share/software-center/softwarecenter/ui/gtk3/views/catview_gtk.py", line 266, in _build_homepage_view
    self._append_whats_new()
  File "/usr/share/software-center/softwarecenter/ui/gtk3/views/catview_gtk.py", line 430, in _append_whats_new
    whats_new_cat = self._update_whats_new_content()
  File "/usr/share/software-center/softwarecenter/ui/gtk3/views/catview_gtk.py", line 419, in _update_whats_new_content
    docs = whats_new_cat.get_documents(self.db)
  File "/usr/share/software-center/softwarecenter/db/categories.py", line 124, in get_documents
    nonblocking_load=False)
  File "/usr/share/software-center/softwarecenter/db/enquire.py", line 317, in set_query
    self._blocking_perform_search()
  File "/usr/share/software-center/softwarecenter/db/enquire.py", line 212, in _blocking_perform_search
    matches = enquire.get_mset(0, self.limit, None, xfilter)
  File "/usr/share/software-center/softwarecenter/db/appfilter.py", line 89, in __call__
    if (not pkgname in self.cache and
  File "/usr/share/software-center/softwarecenter/db/pkginfo_impl/aptcache.py", line 263, in __contains__
    return self._cache.__contains__(k)
AttributeError: 'NoneType' object has no attribute '__contains__'

As you can see in the SystemError part, it seems to have an issue with the one of the repository lists in /var/lib/apt/lists/, so the natural solution as this post suggests is to remove the one it is complaing about or better yet, remove all and run apt-get update to create them again:

sudo rm -rf /var/lib/apt/lists/*
sudo apt-get update

You should be good to go…

Wednesday, May 23rd, 2012 Linux, Ubuntu 5 Comments

Search

 

Archive

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

Other