|

First Android application

Or so… if numerous "hello world" applications doesn’t count. Well, time flies, it has been three weeks since the android class I was attending ended. Many thanks to MultiUni for organizing the event! I joined the class simply because of the boldness of whoever bring the idea of a learning community into motion, especially in Vietnam; and I know many share the same feeling with me. It’s a great opportunity to meet more people and get updates on what’s going on. One of my teachers wanted to do this years ago in the form of a university’s computing club but it seems the idea never saw the light of day – he is now a PhD or more accurately, Doctor of Science. He have had more important things to devote his time for 🙂

1.0 A half-user, half-programmer’s rant

Being short doesn’t stop the class from giving me a good head start on Android. Basically the Android’s application share much in common with J2ME, a cousin sharing the same root with Android in the Java family. However, Android have a more clearly defined framework – all naming are done according to convention, similar functions from different packages doesn’t have drastically different names like Java. The Dalvik debugging monitor server (with which you can interact through the DDMS view installed with Android Development Tools) is better integrated into the IDE and easier to use than its J2ME’s counterpart. All rounded up, Android is a fantastic platform to start mobile programming!

But being good doesn’t mean things are automatically going to be great for you. A while back I blogged about how frustrating Symbian programming is and predicted its demise. Well, that came true when the iPhone started to take up market share. Ironically that doesn’t mean I am good at predicting market trends but the exact opposite! Apparently hypes and marketing niches have more to do with the success of a product than technical feasibility.

smartphone_market_share_3Q09_trend

Smartphone market share trends, via Gartner and Arstechnica

The iPhone replaced Symbian’s complex model with an arcane platform! Did you know that you need a Mac to be able to program the iPhone? And while Apple started to dwarf Nokia on the mobile market, the first Android phones started to came out. Even a developer community such as the class I were in have only 3 Android phones and though so, they weren’t used for daily tasks like calling or texting :/

Technology takes time to be adapted, but Android just doesn’t have the "coolness" of the iPhone. You can’t impress your girlfriend telling her "I have the latest Google’s touch screen phone with fancy location based features" – she’ll simply ask "isn’t Google a search engine"?

That being said, the future is not bleak for Android. Google, after all is also a big company; and most importantly, they are driven by innovation in technology, just like Apple is driven by innovation in design and user interface. This is going to be an interesting battle and who knows? Maybe you are picking the winning side right now 😉

2.0 The application

Okay, as the title of this post have stated, it’s not an application that will make you coffee to impress your girl (or guy) but rather a demonstrative application on how easy it is to perform system tasks with Android.

The first objective is to make an application that displays pictures from a list. For simplicity’s sake, this will be an array of URLs; of course this can easily be replaced with an RSS feed. Secondly, when the user selects one of those pictures, the application will save it to the device and set it as the wallpaper.

2.1 Display

According to the instruction given, I should have made an application that displays one picture at a time and three buttons to flip between pages and set the wallpaper, like this:

ui1

But being such a busybody I took a scan on the Android samples that comes with the SDK. Fortunately there’s a gallery application using the ImageSwitcher control that looks substantially better:

android_api_imageswitcher

Besides the look, the ImageSwitcher interface also have a view initializing method, GetView so you can implement your own image generating procedure. This is especially useful for an advanced function: caching images.

public View getView(final int position, View convertView, ViewGroup parent)

ImageSwitcher is not a collection of images but rather a collection of ImageView controls, this allows for greater flexibility: you can customize how each image is rendered. For examples, odd images have a white border and even ones have a black border.

As you may have known, mobile devices have significantly tighter memory limit than full-pledged computing devices. Android phones is not an exception. You can only load like 10 640×480 pictures before hitting an “out of memory” exception. In this application I will cache eight images at a time. The caching algorithm is based on the priority queue principle: any time an image is used (get displayed either as the current image or in the preview line), it is moved to the top of the queue, images at the bottom of the queue are disposed to make room for new ones as necessary.

// 1 for view and 8 for scrolling
private final int cacheThreshold = 8;	
private LinkedList imageCache = new LinkedList();

The cache’s data structure 

It appears that all objects in Android have a Tag attribute so you can attach anything you want to them. I used that to attach the position of the image contained in the ImageView control in the list. By looking at this value you can easily determine when the ImageView is being displayed and move it up the queue.

		public View getView(final int position, View convertView,
				ViewGroup parent) {

			// Search if the cache already have the image,
			// this is better implemented as some kind of
			// comparison operator, but we don't have time
			// to look into that right now

			// Convert the queue to an array for easier iteration
			for (int i = 0; i < imageCache.size(); i++) {
				ImageView temp = imageCache.get(i);
				String imageTag = (String) temp.getTag();
				// Why string and not just position?
				// It will be easier to implement image loading
				// from the file system (images will be referred
				// to by path)
				if (imageTag.compareTo(imageLinks[position]) == 0) {
					// Increase priority for the returned item
					imageCache.remove(i);
					imageCache.addFirst(temp);
					return temp;
				}
			}

			// Load a new image if not cached
			while (imageCache.size() >= cacheThreshold)
			{
				imageCache.getLast().destroyDrawingCache();
				imageCache.removeLast();
			}
			final ImageView newView = new ImageView(mContext);

			newView.setAdjustViewBounds(true);
			newView.setLayoutParams(new Gallery.LayoutParams(
					LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

			// Load the first image with blocking routine to make sure it will display
			if (firstImage) {
				firstImage = false;
				BitmapInfo mResults = BitmapDownloader
					.downloadBitmap(imageLinks[position]);
				newView.setImageBitmap(mResults.getBitmap());
			}
			else
			{
				// Uses the asynchronous routine for other images
				Thread t = new Thread() {
					public void run() {
	
						BitmapInfo mResults = BitmapDownloader
								.downloadBitmap(imageLinks[position]);
						mResults.setToAssign(newView);
						Message downloadedMessage = new Message();
						downloadedMessage.setTarget(mHandler);
						downloadedMessage.what = MESSAGE_TYPE_WALLPAPER_DOWNLOAD_COMPLETE;
						downloadedMessage.obj = mResults;
						mHandler.sendMessage(downloadedMessage);
					}
				};
				t.start();
			}

			newView.setTag(imageLinks[position]);
			// Add the changed entry back to the cache
			imageCache.addFirst(newView);

			return newView;

		}

		private Context mContext;
		private Handler mHandler = new Handler(new Callback() {

			@Override
			public boolean handleMessage(Message msg) {
				if (msg.what == MESSAGE_TYPE_WALLPAPER_DOWNLOAD_COMPLETE) {
					final BitmapInfo downloaded = (BitmapInfo) msg.obj;
					// Fail? Try again, memory will be freed in a moment...
					if (downloaded.getBitmap() == null) {
						Thread t = new Thread() {
							public void run() {

								BitmapInfo mResults = BitmapDownloader
										.downloadBitmap(downloaded.getIdentifier());
								mResults.setToAssign(downloaded.getToAssign());
								Message downloadedMessage = new Message();
								downloadedMessage.setTarget(mHandler);
								downloadedMessage.what = MESSAGE_TYPE_WALLPAPER_DOWNLOAD_COMPLETE;
								downloadedMessage.obj = mResults;
								mHandler.sendMessage(downloadedMessage);
							}
						};
						t.start();
					}
					else
						downloaded.getToAssign().setImageBitmap(
							downloaded.getBitmap());
				}
				return true;
			}
		});

		// Control first image's loading
		private boolean firstImage = true;
		private static final int MESSAGE_TYPE_WALLPAPER_DOWNLOAD_COMPLETE = 3;

The full caching & downloading routine

The Handler in Android is similar to the action or key press listener in java, it handles messages sent to it and have access to all the private variables inside the object it’s placed in. But unlike Java it’s not bound to the object and one object can have multiple handler. However it’s worth noting that Handler’s processing is blocking and it will halt the thread it’s running on so it’s not a good idea to use them for long activities like data transmission. In this case I:

  1. Send the download request to the Handler
  2. The handles process the download request and creates a new thread to download the image.
  3. When the thread is done, is sends a message back to the Handler. Note that I attached the image to that message using Message.obj
  4. Finally the Handler assigns the downloaded image back into the View

Oh, and accessing the internet requires permission from the user (so you won’t send out sensitive data). To get permission you’ll have to ask for it, add those to the manifest file:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

 

2.2 Set wallpaper

In android, to make a button do something, you set its listener, like this

final Button btnSetWallpaper = (Button) findViewById(R.id.btnsetwallpaper);
		btnSetWallpaper.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View arg0) {
				// Save dialog
				AlertDialog.Builder builder = new AlertDialog.Builder(arg0
						.getContext());
				builder.setMessage("Do you want to save this image?")
						.setCancelable(false).setPositiveButton("Yes",
								wallpaperDialogHandle).setNegativeButton(
								"Cancel", wallpaperDialogHandle)
						.setNeutralButton("No", wallpaperDialogHandle);
				AlertDialog alert = builder.create();
				alert.show();

			}
		});

And then define what the listener will do:

public DialogInterface.OnClickListener wallpaperDialogHandle = new DialogInterface.OnClickListener() {

		@Override
		public void onClick(DialogInterface dialog, int which) {
			switch (which) {
			case DialogInterface.BUTTON_POSITIVE: // Yes
			case DialogInterface.BUTTON_NEUTRAL: // No
				WindowManager mWinMgr = (WindowManager) getBaseContext()
						.getSystemService(Context.WINDOW_SERVICE);
				int displayWidth = mWinMgr.getDefaultDisplay().getWidth();
				int displayHeight = mWinMgr.getDefaultDisplay().getHeight();

				Bitmap newwallpaper = Bitmap.createBitmap(displayWidth,
						displayHeight, Config.ARGB_8888);
				Canvas myCanvas = new Canvas(newwallpaper);
				Gallery g = (Gallery) findViewById(R.id.gallery);
				// Draw the image to make sure the aspect ratio match
				((ImageView) g.getSelectedView()).getDrawable().draw(myCanvas);
				try {
					setWallpaper(newwallpaper);
				} catch (IOException e) {
					e.printStackTrace();
				}
				// Save file
				if (DialogInterface.BUTTON_POSITIVE == which) {
					Date date = new Date();
					java.text.DateFormat dateFormat = new java.text.SimpleDateFormat(
							"yyyyMMddhhmmss");
					String dateTimeString = dateFormat.format(date);
					try {
						FileOutputStream fos = new FileOutputStream(new File("/sdcard/" + dateTimeString + ".jpg"));

						newwallpaper.compress(CompressFormat.JPEG, 75, fos);

						fos.flush();
						fos.close();
					} catch (Exception e) {
						Log.e("MyLog", e.toString());
					}
				}
			default: // Cancel
				break;
			}
		}
	};

The important part is inside the try-catch block: setWallpaper(). I also added some image resizing (to make the wallpaper fit the screen) and save to device routine. I’m specifying “/sdcard/” in the FileOutputStream constructor to write to external storage. If you don’t specify this Android will write to your application’s private storage on device memory. Device memory is often much smaller than its external counterpart so it’s best to reserve it for sensitive data you want nobody else to read only.

2.3 Splash screen

Finally, to show tribute to the nice guidance given by the instructor, I have to add a splash screen with MultiUni logo eh? So I added the splash screen activity and change the startup activity to it

public class SplashScreenActivity extends Activity {
	public static final int HANDLER_MSG_WAIT = 1;
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.splash_screen);
        
        mHandler.sendEmptyMessageDelayed(HANDLER_MSG_WAIT, 2000);
    }
    
	Handler mHandler = new Handler() {

		@Override
		public void handleMessage(Message msg) {
			super.handleMessage(msg);
			Intent intent = new Intent(getApplicationContext(), 
					WallpaperTool.class);
			startActivity(intent);
			finish();
		}
    	
    };
    
}

See the Intent part? it’s used to call another activity. And that’s it!

You deserve something after all that reading, right? 😛 You can grab the source and compiled binary here (for Android 1.6)

Leave a Reply

Your email address will not be published. Required fields are marked *