Pages

Monday 23 January 2012

Creating a custom Android Intent Chooser

I recently added a feature to my Zedusa image resizer application that required me to write a custom intent chooser. The reason for this was so I could add a checkbox on the list so the user could choose to make the application they selected the default one going forward.


I thought I'd share a snippet of the code on my blog as it could be useful for some. Feel free to ask any questions about the inner workings of it, otherwise I'll leave it up to you to read and understand. Please be aware I stripped some code out so I haven't actually ran the exact code below on my phone.

public void startDefaultAppOrPromptUserForSelection() {
 String action = Intent.ACTION_SEND;

 // Get list of handler apps that can send
 Intent intent = new Intent(action);
 intent.setType("image/jpeg");
 PackageManager pm = getPackageManager();
 List<ResolveInfo> resInfos = pm.queryIntentActivities(intent, 0);

 boolean useDefaultSendApplication = sPrefs.getBoolean("useDefaultSendApplication", false);
 if (!useDefaultSendApplication) {
  // Referenced http://stackoverflow.com/questions/3920640/how-to-add-icon-in-alert-dialog-before-each-item

  // Class for a singular activity item on the list of apps to send to
  class ListItem {
   public final String name;
   public final Drawable icon;
   public final String context;
   public final String packageClassName;
   public ListItem(String text, Drawable icon, String context, String packageClassName) {
    this.name = text;
    this.icon = icon;
    this.context = context;
    this.packageClassName = packageClassName;
   }
   @Override
   public String toString() {
    return name;
   }
  }

  // Form those activities into an array for the list adapter
  final ListItem[] items = new ListItem[resInfos.size()];
  int i = 0;
  for (ResolveInfo resInfo : resInfos) {
   String context = resInfo.activityInfo.packageName;
   String packageClassName = resInfo.activityInfo.name;
   CharSequence label = resInfo.loadLabel(pm);
   Drawable icon = resInfo.loadIcon(pm);
   items[i] = new ListItem(label.toString(), icon, context, packageClassName);
   i  ;
  }
  ListAdapter adapter = new ArrayAdapter<ListItem>(
    this,
    android.R.layout.select_dialog_item,
    android.R.id.text1,
    items){

   public View getView(int position, View convertView, ViewGroup parent) {
    // User super class to create the View
    View v = super.getView(position, convertView, parent);
    TextView tv = (TextView)v.findViewById(android.R.id.text1);

    // Put the icon drawable on the TextView (support various screen densities)
    int dpS = (int) (32 * getResources().getDisplayMetrics().density   0.5f);
    items[position].icon.setBounds(0, 0, dpS, dpS);
    tv.setCompoundDrawables(items[position].icon, null, null, null);

    // Add margin between image and name (support various screen densities)
    int dp5 = (int) (5 * getResources().getDisplayMetrics().density   0.5f);
    tv.setCompoundDrawablePadding(dp5);

    return v;
   }
  };

  // Build the list of send applications
  AlertDialog.Builder builder = new AlertDialog.Builder(this);
  builder.setTitle("Choose your app:");
  builder.setIcon(R.drawable.dialog_icon);
  CheckBox checkbox = new CheckBox(getApplicationContext());
  checkbox.setText(getString(R.string.enable_default_send_application));
  checkbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

   // Save user preference of whether to use default send application
   @Override
   public void onCheckedChanged(CompoundButton paramCompoundButton,
     boolean paramBoolean) {
    SharedPreferences.Editor editor = sPrefs.edit();
    editor.putBoolean("useDefaultSendApplication", paramBoolean);
    editor.commit();
   }
  });
  builder.setView(checkbox);
  builder.setOnCancelListener(new OnCancelListener() {

   @Override
   public void onCancel(DialogInterface paramDialogInterface) {
    // do something
   }
  });

  // Set the adapter of items in the list
  builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
   @Override
   public void onClick(DialogInterface dialog, int which) {
    SharedPreferences.Editor editor = sPrefs.edit();
    editor.putString("defaultSendApplicationName", items[which].name);
    editor.putString("defaultSendApplicationPackageContext", items[which].context);
    editor.putString("defaultSendApplicationPackageClassName", items[which].packageClassName);
    editor.commit();

    dialog.dismiss();

    // Start the selected activity sending it the URLs of the resized images
    Intent intent;
    intent = new Intent(Intent.ACTION_SEND);
    intent.setType("image/jpeg");
    intent.setClassName(items[which].context, items[which].packageClassName);
    startActivity(intent);
    finish();
   }
  });

  AlertDialog dialog = builder.create();
  dialog.show();


 } else { // Start the default send application

  // Get default app name saved in preferences
  String defaultSendApplicationName = sPrefs.getString("defaultSendApplicationName", "<null>");
  String defaultSendApplicationPackageContext = sPrefs.getString("defaultSendApplicationPackageContext", "<null>");
  String defaultSendApplicationPackageClassName = sPrefs.getString("defaultSendApplicationPackageClassName", "<null>");
  if (defaultSendApplicationPackageContext == "<null>" || defaultSendApplicationPackageClassName == "<null>") {
   Toast.makeText(getApplicationContext(), "Can't find app: "  defaultSendApplicationName  
     " ("   defaultSendApplicationPackageClassName   ")", Toast.LENGTH_LONG).show();

   // don't have default application details in prefs file so set use default app to null and rerun this method
   SharedPreferences.Editor editor = sPrefs.edit();
   editor.putBoolean("useDefaultSendApplication", false);
   editor.commit();
   startDefaultAppOrPromptUserForSelection();
   return;
  }

  // Check app is still installed
  try {
   ApplicationInfo info = getPackageManager().getApplicationInfo(defaultSendApplicationPackageContext, 0);
  } catch (PackageManager.NameNotFoundException e){
   Toast.makeText(getApplicationContext(),  "Can't find app: "   defaultSendApplicationName  
     " ("   defaultSendApplicationPackageClassName   ")", Toast.LENGTH_LONG).show();

   // don't have default application installed so set use default app to null and rerun this method
   SharedPreferences.Editor editor = sPrefs.edit();
   editor.putBoolean("useDefaultSendApplication", false);
   editor.commit();
   startDefaultAppOrPromptUserForSelection();
   return;
  }

  // Start the selected activity
  intent = new Intent(Intent.ACTION_SEND);
  intent.setType("image/jpeg");
  intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
  intent.setClassName(defaultSendApplicationPackageContext, defaultSendApplicationPackageClassName);
  startActivity(intent);
  finish();
  return;
 }
}

Friday 20 January 2012

ARWedding - My first AR app


I had a bit of a hiatus on AR stuff while I got caught up doing some development for other Android apps. I'm getting married in a week so I thought I'd kick off by making an adaption of Qualcomm's QCAR (since renamed Vuforia) sample app ImageTargets into a novelty wedding gift. It simply shows a photo of the two of us for the splash screen and a rose as the 3D model. I'll be releasing both Android and iPhone versions which you can find from the corresponding links on the top-right of my blog. The links to the projects' sources are below.

I won't go into detail of how I did what as that's just a little bit too time consuming with lots of wedding preparations to do;-) If you do have any specific questions feel free to drop a comment and I'll try and reply when I can.

The customizations I did to the Android/iPhone verions are:

  • Custom app icon
  • Custom 3D model of a rose to replace the teapot
  • Inserted rose coloured textures (ideally I'd want to have multiple textures; one for the green of the stalk and another for the red of the flower but that turned out to be a more advanced topic that I decided to put aside for a rainy day)
  • Changed the trackables (markers) to a QR code (see below) and the faces on 1000, 5000 and 10,000 yen Japanese notes
  • Played with the kObjectScale to get a better sized rose and rotated the projection matrix to make the rose appear as if it was standing upright

Here are the links to the projects' source code. I was using the QCAR SDK 1.5.4 beta1, the latest at the time of coding.

  • Android project for Eclipse. I was using Eclipse Helios testing on my Nexus S 2.3.4 phone and my Asus Transformer EEE TF101 tablet running Android 3.2. You may need to edit the NDK settings so it can find the QCAR SDK properly.
  • iPhone project for Xcode. I was using Xcode 3.2.3 and tested on my jailbroken iPhone 3GS running iOS 4.1 and on my iPhone4 running iOS 5. The project was in the same folder as the QCAR SDK sample projects, I'm not sure whether this would involve some settings changes if you use a different folder location.

Besides the Japanese notes you can also use the following QR code as a marker. The colour of the rose associated with the QR code is red whereas the money trackables correspond with a red-green coloured texture.

Friday 6 January 2012

Error 9015 on the Oneworld.com Round the World site

I thought it might be time for my first non-programming article. My wife and I are going to be doing a round the world trip for our honeymoon next month. I've been spending literally hours at One World's online RTW interactive planner trying to work out the cheapest and best route. It's quite a fun tool just to play with as there are so many destinations and heaps of permutations. The route we eventually settled is the map below start in Seoul:

 You can see the little message at the bottom which basically means everything's validated - ie. all the flights/stopovers and paths don't break any of One World's RTW ticket rules and regulations. And trust me there's heaps. From a programmer's point of view I appreciate how complex such a system is and trying to code in the conditions for all that business logic must be a nightmare designing and coding. With a system this big it's inevitable that there will be some bugs in the system.

Anyway, onto the title of this post. After I validated my journey, confirmed the price etc. I went ahead with entering our personal info and then my credit card info. On the very last step of purchasing it sat there for about 3-5 minutes processing and then came back with error "9015" telling me to contact my Travel Assistance Desk for help.

The problem is this site doesn't seem to have a single point of contact for help. Eventually I rang American Airlines and got through to their RTW hotline (phone number +1800 247 3247) and the lady I spoke with was at least familiar with the system somewhat and had a list of common error codes on hand. Unfortunately 9015 wasn't in that list. She tried manually booking my ticket for me but as the ticket wasn't starting in the US, it was starting in South Korea, they needed to get a quote from their office there. The base tariffs and taxes etc. are based on how many continents you stop in and differ greatly depending on the country you start in (Seoul was a very cheap place to start for people starting in Asia - I just catch the boat to Korea and hop on the RTW from there:).

To cut a long story short, the crux of the story is I wasn't able to purchase my RTW ticket in Korea using the online planner with my credit card because I needed either a) a Korean credit card (the billing address must be there; mine was Japanese) or b) turn up in person with my credit card to the Seoul office of American Airlines which was a non-option because I won't be in Seoul until 2 days before my world trip begins.

I now am guessing that the 9015 error was due to my credit card's billing address not being in the same country as the starting point of my journey. In the end I booked on the phone through a friend's workplace in London for a sweet deal. The guys @ www.roundtheworldexperts.co.uk are great!