Posts tagged with hack

Customizing individual share services in a ShareActionProvider on Android

In theory, sharing on Android is simple. Because of the system of Intents, you can prepare your shareable content, like an image, or text, and then easily show a list of installed apps which are able to handle this type of content. For example, if you’re sharing a photo, apps like Instagram, Twitter and the Gallery app will be available in the list. No additional coding is required for each sharing service!

  1. Intent i=new Intent(android.content.Intent.ACTION_SEND);
  2. i.setType("text/plain");
  3. i.putExtra(android.content.Intent.EXTRA_SUBJECT,"My cool app");
  4. i.putExtra(android.content.Intent.EXTRA_TEXT, "Here’s some content to share");
  5. startActivity(Intent.createChooser(i,"Share"));

But what if you want to customise the content depending on the chosen service? For example, if the user chooses Twitter you might want to shorten the text you’re sharing. Facebook on Android also has a long-running bug that means it won’t share text/plain content shared via an Intent… it only works for links.

Perhaps we’d like to use some different logic when our user clicks Facebook, for example using the Facebook Android SDK we could invoke an Open Graph sharing dialog.

Unfortunately, this is not easy. If you’ve followed the Android tutorial on adding an easy share action to the action bar then you’ll have a ShareActionProvider which creates a share button and dropdown for your action bar.

sap

The documentation is rather contradictory about whether you can customise ShareActionProvider’s behaviour. There’s a promising looking listener called setOnShareTargetSelectedListener, described here:

Sets a listener to be notified when a share target has been selected. The listener can optionally decide to handle the selection and not rely on the default behaviour which is to launch the activity.

So we might think to check the type of the intent in the listener, and run some custom behaviour for certain share types.

However the documentation goes on to say that

"Modifying the intent is not permitted and any changes to the latter will be ignored. You should not handle the intent here. This callback aims to notify the client that a sharing is being performed, so the client can update the UI if necessary."

The return result is ignored. Always return false for consistency.

It turns out if we try to add some custom code in setOnShareTargetSelectedListener, the custom code is run, but the standard share intent is also launched :(

Luckily since Android is open source, we can dig around in the source code to find out what’s going on.

Here’s the source code for the ShareActionProvider class in the v7 support library on Github.

Notice the class at the bottom ShareActivityChooserModelPolicy, which calls the listener, but then returns false regardless. Returning true from this method would allow us to handle the intent, without invoking the default behaviour.

  1. private class ShareActivityChooserModelPolicy implements OnChooseActivityListener {
  2. @Override
  3. public boolean onChooseActivity(ActivityChooserModel host, Intent intent) {
  4. if (mOnShareTargetSelectedListener != null) {
  5. mOnShareTargetSelectedListener.onShareTargetSelected(ShareActionProvider.this,intent);
  6. }
  7. return false;
  8. }
  9. }

We can’t easily subclass ShareActionProvider to override this behaviour, but what we can do is make a complete copy of the class and implement our own custom behaviour!

Copy the entire source file into your app, changing the package declaration at the top, and optionally the class name, for example to RDShareActionProvider.

Implement a new listener

  1.  
  2. private OnShareListener mOnShareListener; //also need to add getter and setter
  3.  
  4. public interface OnShareListener {
  5. /**
  6. * Called when a share target has been selected. The client can
  7. * decide whether to perform some action before the sharing is
  8. * actually performed OR handle the action itself.
  9.  
* * @param source The source of the notification. * @param intent The intent for launching the chosen share target. * @return Return true if you have handled the intent. */ public boolean willHandleShareTarget(RDShareActionProvider source, Intent intent); }

Time to re-implement the ShareActivityChooserModelPolicy using our new more powerful callback.

  1.  
  2. private class ShareActivityChooserModelPolicy implements OnChooseActivityListener {
  3. @Override
  4. public boolean onChooseActivity(ActivityChooserModel host, Intent intent) {
  5. if (mOnShareListener != null) {
  6. boolean result = mOnShareListener.willHandleShareTarget(
  7. RDShareActionProvider.this, intent);
  8. return result;
  9. }
  10. return false;
  11. }
  12. }
  13.  

We're in the home straight! Now we need to change the reference in the menu XML to our new class name.

  1. <item
  2. android:id="@+id/action_share"
  3. android:title="@string/menu_share"
  4. android:icon="@drawable/ic_action_share"
  5. myapp:showAsAction="always"
  6. myapp:actionProviderClass="com.myapp.RDShareActionProvider"

Finally we can implement our listener. We can check the package name of the intent, each sharer will have a different one, depending on the app. For Facebook, it’s com.facebook.katana.

  1.  
  2. mShareActionProvider.setOnShareListener(new OnShareListener() {
  3. @Override
  4. public boolean willHandleShareTarget(RDShareActionProvider source, Intent intent) {
  5. if (intent.getComponent().getPackageName().equalsIgnoreCase("com.facebook.katana")) {
  6. //just showing a toast for now
  7. //we could also manually dispatch an intent, based on the original intent
  8. Toast.makeText(self, "Hey, you're trying to share to Facebook", Toast.LENGTH_LONG).show();
  9. return true;
  10. } else {
  11. return false; //default behaviour.
  12. }
  13. }
  14. });
  15.  

Finally, we have control over individual sharers!

Generating your holiday photos automatically with the iPhone location database

As you may have read, the iPhone 4 stores a database of your location over time, and it's simple to access this database on your computer. Notwithstanding the privacy implications, it provides an interesting source of data to play with!

I've spent the last few months travelling while working for ReignDesign, and my iPhone has been faithfully recording the trip. I extracted the consolidated.db file which the iPhone stores in its backup, and wrote a small Python script to extract my location history. The script finds the first recorded location for each day, and then:
1. Looks up the city name and country using Google's Geocoding API
2. Searches for photos with the Flickr API taken close to the latitude/longitude I was at, on that day

Note that not every day has a location recorded. When I was in Tonga and Samoa, I didn't have a SIM card. Since the consolidated.db file uses cell towers to calculate location, not GPS, there are gaps. Also, sometimes there are no photos on Flickr for a (location, date) pair.

With those caveats, put it together and you have what I call "the lazy man's holiday photos":

View my holiday photos >>

Fixing “Upload Aborted or Timed Out” errors in iTunes Connect

When you're uploading screenshots to iTunes Connect, you may run into the error "Upload Aborted or Timed Out". This can be very frustrating if you're on a slow connection. Sometimes, retrying the upload may succeed, or converting the files to JPGs.

This is not ideal: we'd like to be able to upload nice hi-res iPad screenshots, even on a slow connection. I did some digging in iTunes Connect's Javascript. It turns out it uses a component called LCUploader to handle the uploading. Deep in the bowels of a file called lc_ajaxcomponents.js, we find this code:

self.timerId = setInterval(function() { self.checkUploadHeartbeat(); }, 10000);
...
this.checkUploadHeartbeat = function() {
        if (this.lastProgressDate == 0) { return; }
        
        var now = new Date().getTime();
        var diff = now - this.lastProgressDate;
        if (diff > 10000) {
            
            // We have waited more than 10 seconds without any bytes being pushed
            clearInterval(this.timerId);
            // Mark the request as being aborted
            this.aborted = true;
            // And finally abort the XHR
            this.xhrRequest.abort();
            this.displayErrorMessage("Upload Aborted or Timed Out.");
            this.reset();
            this.stopSpinner();

Aha! It appears that this check is incorrectly causing the upload to time out after 10 seconds. We can override this function at runtime. Paste the following code into your browser's address bar:

javascript:LCUploader.prototype.checkUploadHeartbeat = function() {};void(0);

This overrides the function with an empty one. Now we're able to upload larger files with no issues, even on a bad wifi connection in Tonga ;)

Weekend hack: Posting to Twitter via SMS in China

twitter-smsTwitter provides phone numbers to allow you to update your status via SMS. Unfortunately, local numbers are only available in a few countries: the US, Canada and India. Elsewhere, you need to text their international number in the UK, which can be quite expensive. ReignDesign is based in Shanghai, China - so how to update Twitter for the price of a local SMS? Via a local Twitter-like service Fanfou.com, and a simple PHP script, it's possible!
Continue reading...