Saturday, December 14, 2013

Google Drive API - some helpful tips and code

I am not sure why, but there are not a lot of examples how to use Google Drive API from Android. If you're wondering about it, or you look for a working code, here it is...

I assume you've already went successfully through the Google Drive SDK Quickstart - https://developers.google.com/drive/quickstart-android .

1) If you're using Eclipse as your IDE and you don't enjoy the command line, for step 1 in the Quickstart, you can find the right SHA1 fingerprint in Window->Preferences -> Android -> Build.

2) In Google Cloud Console (https://cloud.google.com/console) enable both Drive API and Drive SDK

3) Enabling Google Drive for your Activity. The code assumes that the main purpose of the activity is to synchronize your content (I called it BackupActivity - to backup all the content from the app to the Google Drive).

a. In your Activity put the following:

// change the following to your Activity name
private final static String TAG = "BackupActivity";

static final int REQUEST_ACCOUNT_PICKER = 1;
static final int REQUEST_AUTHORIZATION = 2;

private static Drive service;
private GoogleAccountCredential credential;

b. In the onCreate put:
  
credential = GoogleAccountCredential.usingOAuth2(this, 
             Arrays.asList(DriveScopes.DRIVE));
startActivityForResult(credential.newChooseAccountIntent(), 
             REQUEST_ACCOUNT_PICKER);      

c. Add the following methods:
  
private Drive getDriveService(GoogleAccountCredential credential) {
  return new Drive.Builder(AndroidHttp.newCompatibleTransport(), 
    new GsonFactory(), credential)
    .setApplicationName(getApplicationInfo().name).build();
 }

@Override
 protected void onActivityResult(final int requestCode, final int resultCode, 
    final Intent data) {
  switch (requestCode) {
  case REQUEST_ACCOUNT_PICKER:
   if (resultCode == RESULT_OK && data != null && data.getExtras() != null) {
    String accountName = data.getStringExtra(
       AccountManager.KEY_ACCOUNT_NAME);
    if (accountName != null) {
     credential.setSelectedAccountName(accountName);
     service = getDriveService(credential);
     backupData();
    }
   }
   break;
  case REQUEST_AUTHORIZATION:
   if (resultCode == Activity.RESULT_OK) {
    backupData();
   } else {
    startActivityForResult(credential.newChooseAccountIntent(), 
             REQUEST_ACCOUNT_PICKER);
   }
   break;
  }
 }
d. Now you need to fill your backupData() method, I suggest the following structure:
 
private void backupData() {
 // run backup in the background
 Thread t = new Thread(new Runnable() {
 @Override
 public void run() {
  try {
           // your backup logic here
  } catch (UserRecoverableAuthIOException e) {
    startActivityForResult(e.getIntent(), REQUEST_AUTHORIZATION);
  } catch (final Exception e) {
   // runOnUIThread is required of Toast
   runOnUiThread(new Runnable() {    
   @Override
    public void run() {
     // TODO: (for you) add an error_backup string to the resources
     Toast.makeText(getApplicationContext(),
     getResources().getString(R.string.error_backup) e.getMessage(), 
     Toast.LENGTH_LONG).show();
     }
     });
     Log.e(TAG, "Error while data backup: " + e.getMessage());
    }
   }
 });
 t.start();
 finish();
}
e. Now you can wonder how to push data to the Google Drive. Below you will find some utility methods that I've created for this purpose (this is copy paste from real app, so sorry if something is missing - let me know in such case :)). I hope the in-code comments is enough to understand what is going on...
 
 private final static String TAG = "GoogleDriveHelper";
 public final static String BINARY_FILE_IMG_MIME_TYPE = "image/png";
 public final static String BINARY_FILE_AUDIO_MIME_TYPE = "audio/amr";

/**
  * Get File handler for folder with provided parameters. If it doesn't exist, 
  * it will be created.
  * @param service
  * @param folderName
  * @param parentId
  * @return file that was created or found according to provided parameters
  * @throws UserRecoverableAuthIOException
  */
 public static File getOrCreateFolder(Drive service, String folderName, String parentId)
   throws UserRecoverableAuthIOException {
  File result = null;
  // check if the folder exists already
  try {
   String query = "mimeType='application/vnd.google-apps.folder' 
     and trashed=false and title='" + folderName
     + "'";
   // add parent param to the query if needed
   if (parentId != null) {
    query = query + " and '" + parentId + "' in parents";
   }
   Files.List request = service.files().list().setQ(query);
   FileList files = request.execute();
   if (files.getItems().size() == 0) {
    // File's metadata.
    File body = new File();
    if (parentId != null) {
     ParentReference parent = new ParentReference();
     parent.setId(parentId);
     java.util.List parents = 
       new ArrayList();
     parents.add(parent);
     body.setParents(parents);
    }
    body.setTitle(folderName);
    body.setMimeType("application/vnd.google-apps.folder");
    result = service.files().insert(body).execute();
   } else {
    result = files.getItems().get(0);
   }
  } catch (UserRecoverableAuthIOException ue) {
   throw ue;
  } catch (Exception e) {
   Log.e(TAG, "Exception while trying to getOrCreateFolder, folderName: " 
     + folderName + " parentId:"
     + parentId + " exception:" + e.getMessage());
  }
  return result;
 }

/**
  * Create or update a remote file out of a local binary file. Remote file gets 
  * updated ONLY if file size is
  * different (e.g. when previous upload failed)
  * @param service
  * @param localPath
  * @param parentId
  * @return File object for the remote file or null if the local file doesn't 
  * exist or any other error occurs.
  * @throws UserRecoverableAuthIOException
  */
 public static File createOrUpdateFileFromFile(Drive service, String localPath, 
   String parentId)
   throws UserRecoverableAuthIOException {
  File result = null;
  try {
   // check if the local file exists, if it doesn't, return null
   java.io.File localFile = new java.io.File(localPath);
   if (!localFile.exists()) {
    Log.w(TAG, "File doesnt exist, so skipping createOrUpdate: " + 
     localPath);
    return null;
   }
   String fileName = localFile.getName();
   String fileMime;
   if (fileName.endsWith("amr")) {
    fileMime = BINARY_FILE_AUDIO_MIME_TYPE;
   } else {
    fileMime = BINARY_FILE_IMG_MIME_TYPE;
   }
   // check if given file exists
   String query = "trashed=false and title='" + fileName + "' and '" + 
    parentId + "' in parents";
   Files.List request = service.files().list().setQ(query);
   FileList files = request.execute();
   if (files.getItems().size() == 0) {
    // file doesnt exist - create a new one
    File body = new File();
    // set parent
    ParentReference parent = new ParentReference();
    parent.setId(parentId);
    java.util.List parents = 
     new ArrayList();
    parents.add(parent);
    body.setParents(parents);
    // set properties
    body.setTitle(fileName);
    body.setMimeType(fileMime);
    // push content
    InputStream in = null;
    try {
     in = new BufferedInputStream(new FileInputStream(localFile));
     InputStreamContent content = 
      new InputStreamContent(fileMime, in);
     Drive.Files.Insert insertRequest = 
      service.files().insert(body, content);
     insertRequest.getMediaHttpUploader().
      setDirectUploadEnabled(true);
     result = insertRequest.execute();
    } finally {
     if (in != null) {
      in.close();
     }
    }
   } else {
    // file exists, update it if the one on the server has different 
    // size
    File remoteFile = files.getItems().get(0);
    if (localFile.length() != remoteFile.getFileSize().longValue()) {
     Log.d(TAG, "Performing update of file: " + localPath);
     InputStream in = null;
     try {
      in = new BufferedInputStream(
       new FileInputStream(localFile));
      InputStreamContent content = 
       new InputStreamContent(fileMime, in);
      Drive.Files.Update updateRequest =
       service.files().update(remoteFile.getId(), 
        remoteFile, content);
      updateRequest.getMediaHttpUploader().
       setDirectUploadEnabled(true);
      result = updateRequest.execute();
     } finally {
      if (in != null) {
       in.close();
      }
     }
    } else {
     Log.d(TAG, "Skipping update of file because size is the same:"
       + localPath);
    }
   }
  } catch (UserRecoverableAuthIOException ue) {
   throw ue;
  } catch (Exception e) {
   Log.e(TAG, "Exception while trying to createOrUpdateFile, localPath:" + 
    localPath + " parentId:"
     + parentId + " exception:" + e.getMessage());
  }
  return result;
 }

Friday, November 29, 2013

How to make Logitech Trackball Marble Wheel work

If you bought Trackball Marble from Logitech, the first challenge you encounter is probably related to the lack of the wheel button. Unfortunately the software provided with the device for Windows doesn't help (neither Universal or Auto Search aren't really working as I was expecting).

Internet suggests mostly one option, app called Marble Mouse Scroll Wheel
http://marble-mouse-scroll-wheel.software.informer.com/

To some extent it works, but I wasn't able to make it work in google maps or in picture viewer. Moreover setting where crashing very often (I am running windows 7 64 bits).

Fortunately there is a way to have a semi-wheel button behavior with this trackball, but with a different software - X-Mouse Button Control:

http://www.highrez.co.uk/downloads/XMouseButtonControl.htm

Setup Mouse button 4 and 5 as wheel up and wheel down. Then also update Logitech SetPoint settings to replace the behavior of those button to default.

Voila - now you can emulate wheel with your trackball buttons!

Saturday, October 12, 2013

Android Notifications - lesson learned from implementation

Recently some Conf Call Dialer users asked to add notifications to the application, so (since it seems to be a good idea) I've spent few hours implementing them in the app. Everything went smoothly, except of one rather funny problem, after executing NotificationManager.notify, notifications were not showing up and I was getting all the time the following warning in the LogCat (12345 is the id assigned to the notification):


notify: id corrupted: sent 12345, got back 0

My code seemed to be rather simple (mostly copy paste from the javadoc, with some app specific logic added):
 
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this).
setContentTitle(event.subject).setContentText(event.timeFrom + " " + additionalText);
// ...
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.notify(notificationId, mBuilder.build());

Googling didn't help, so I compare the code to the standard and the only difference was lack of the small icon (well, I didn't have it at this time so I decided I will skip it). And guess what - without a small icon the notification will NOT work and the only message you'll get is the "id corrupted" mentioned above.

The following code works already:
 
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this).
setContentTitle(event.subject).setContentText(event.timeFrom + " " + additionalText).setSmallIcon(R.drawable.ic_action_search);
// ...
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.notify(notificationId, mBuilder.build());

Tuesday, July 9, 2013

Azure Storage Explorer - for all your Azure Storage management needs

If you are like me a heavy Azure user you may try the following tool that in an easy (and faster than the management UI) way allows you to manage content of your storages. Works like a dream, setup takes like 10 seconds.



http://azurestorageexplorer.codeplex.com/

Sunday, April 21, 2013

Rendering AdMob view on Canvas (SurfaceView) in android

If you're wondering how put a working AdMob view into your SurfaceView and are tired of looking in the Internet for solution (somehow most suggestions that I found on forums didnt work), here it is...

Assumptions:
A. We request the ad on creation (you may want to refresh it later though...)
B. The AdView is put on the bottom of the screen
C. It's a production ready code, but if you want to test it - add testDevices to the adRequest
D. You've already set AndroidManifest properly as described in the Getting Started tutorial

1. In the activity that initializes your SurfaceView add a field representing your adView, for example:

 private AdView adView;

2. In the onCreate method of the same activity put the following code:

  // window manager preparation 
  WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
  windowParams.gravity = Gravity.BOTTOM;
  windowParams.x = 0;
  windowParams.y = 0;
  windowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
  windowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
  windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
  windowParams.format = PixelFormat.TRANSLUCENT;
  windowParams.windowAnimations = 0;

  WindowManager wm = getWindowManager();
  // Create the adView
  adView = new AdView(this, AdSize.BANNER, YOU_ADMOB_SITE_ID);

  // Initiate a generic request to load it with an ad
  adView.loadAd(new AdRequest());

  // Add adView to WindowManager
  wm.addView(adView, windowParams);
3. Override the onDestroy to do the cleanup (or add the cleanup code to your onDestroy if you're already doing something there)

 @Override
   public void onDestroy() {
     if (adView != null) {
       adView.destroy();
  }
  super.onDestroy();
 }

Kudos for the concept (and most of the code) to EvilDuck and his post on stackoverflow which was talking about GLSSurfaceView, still the approach was generic enough to work without OpenGL :)

Wednesday, March 13, 2013

ArrayAdapter notifyOnDataSetChanged doesn't refresh underlying ListView

If for any reason you're using ArrayAdapter to feed your ListView on Android you may be struggling with the fact that whatever you do - executing notifyOnDataSetChanged doesn't result in the corresponding ListView getting refreshed. There is one simple solution for this (and no, I am not talking about one that I saw on one forum where it was suggested to recreate the ArrayAdapter, which on a first glance may be a good idea at least until you're happy with the list jumping every time to the top).

So what you need to do to have your ListView updated is:
1. Either execute 

arrayAdapter.remove(itemToRemove); // if you want to delete item from the list
or
arrayAdapter.add(itemToAdd); // if you want to add an item to the list

or
2. Execute
arrayAdapter.clear(); // get rid of all elements
arrayAdapter.addAdd(listOfItems); // add them back to the list

Both work just fine.

Sunday, January 27, 2013

How to join conference calls on Android

Since I am working for a global company for last few years, I spend quite a lot of time on conference calls. Still, joining conference calls especially if you're on a move isn't easy - I always had challenges when trying to memorize participant codes (again - especially while driving :)). But then - when you think about it, we have all the required information in the Android calendar, so it's only a matter of handling it properly.

So here it is - Conference Call Dialer. I hope it will make your life easier (at least when it comes to joining conference calls :))

Additional information about the Conference Call Dialer:

Functionality:
- Retrieve information about meetings from the calendar
- Dial into conference calls with conf code from the invitation
- Manage and use multiple gateways (e.g. of your company and of your customers)
- Review the invitations where conf code could be retrieved
- A widgets for the home screen
- Convenient "My conference call" and "Join current meeting" shortcuts
- Easy sharing of your bridges setup (via link with gmail and via manual import for other channels)