Wednesday, December 19, 2012

Disable text selection in HTML

This is not Android-related, but I do not plan to create a new blog for it:-)
Here is the way how to text selection can be disabled in html. It is usable in case of javascript GUI building.
<body onmousedown="return false">

Tuesday, September 18, 2012

Memory leak when setting Drawable from an AsyncTask

In this article I share my experience with memory leak problem which have relieved me of several hours of my precious time. Maybe I will not tell anything new to you, but maybe I save  someone's minute.

If you are working with ImageView on Android, you probably know, that it is recommended way to update (fill) the ImageView with a Drawable from an AsyncTask, especially in cases that the image for the Drawable have to be downloaded from the Internet first. In the examples below m.photo.getFullSizeImg() represents the method, which downloads the image from Internet and put it into the Drawable object.
During my work I have faced problem with the OutOfMemoryException when I try to start my image -showing-activity several times.

My original, bad one, memory-leak-causing code looked like this:
public class Populator extends AsyncTask<Void, Drawable, Drawable> {
    
    @Override
    protected Drawable doInBackground(Void... params) {
      Drawable preview = null;
      preview = m.photo.getFullSizeImg();
      return preview;
    }
    
    @Override
    protected void onPostExecute(Drawable _preview) {
      imageView.setImageDrawable(_preview);
    }
  }
The problem with this cute code is that variable preview holds the reference to the image Drawable even after onPostExecute() is finished and therefore the garbage collector can't remove the AsyncTask from the memory. So to solve the problem, we have to assign null to the preview variable. that's all and two possible ways of doing it I show bellow.

Well working code using onProgressUpdate() instead of onPostExecute():
public class Populator extends AsyncTask<Void, Drawable, Drawable> {
    
    @Override
    protected Drawable doInBackground(Void... params) {
      Drawable preview = null;
      preview = m.photo.getFullSizeImg();
      publishProgress(preview);
      preview=null;
      return null;
    }
    
    @Override
    protected void onProgressUpdate(Drawable... _preview) {
      imageView.setImageDrawable(_preview[0]);
    }
  }

Another well working code using class variable and onPostExecute():
  public class Populator extends AsyncTask<Void, Void, Void> {
    private Drawable preview = null;
    
    @Override
    protected Void doInBackground(Void... params) {
      preview = m.photo.getFullSizeImg();
      return null;
    }
    
    @Override
    protected void onPostExecute(Void params) {
      imageView.setImageDrawable(preview);
      preview = null;
    }
  }


Hope this will help you.

Tuesday, September 11, 2012

Connect to Picasa album from Android using OAuth2

In this article you will learn how to connect to Picasa service using OAuth2 protocol from Android. This example uses only pure Android API, i.e. no extra jars of libraries are needed.

If you plan to connect to the Picasa albums from your Android application, you probably know, that there is a relatively big number of different "samples" and "guides" which encourages you to use "this protocol" and "that library" saying in turn that this and that is already deprecated and should not be used anymore. Moreover majority of those samples are about different (tasks) services and therefore can not work with Picasa service without adjustments, which are not clear. I have been googling around for about four days as I tried to figure out, what is the proper way of working with Picasa albums, downloading tons of sources, including bags of jars to my "simplesample" project.

Finally I have figured out, that working with Picasa is very easy from Android if you know how to do It.

All we have to do is :

  • Register our application on the Api console https://code.google.com/apis/console/ to get access to the service. Note, that Picasa service is not listed in possible services. It does not matter as you can have 0 active service to work with picasa.
  • Take an OAuth2 authToken,from AccountManager
  • Sign your HttpURLConnection with the authToken


Data that you obtain from Picasa service are in the Atom XML format and therefore there are very simple to parse. For more detailed reference about how to query Picasa see https://developers.google.com/picasa-web/docs/2.0/developers_guide_protocol .

Here goes an activity example. The activity will list albums titles to the LogCat window in Eclipse:

//FIXME This class does not deal with situation when there is not any user account in the device
public class ActivityGallery extends Activity
  implements AccountManagerCallback<Bundle> {
  private final String LOGTAG = getClass().getName();
  /**This is what you have got from the api console https://code.google.com/apis/console/**/
  private final String CLIENT_ID = "123blahblahblah456.com";
  /**
   * Service identification. Note the prefix oauth2 - it is necessary
   * See https://developers.google.com/gdata/faq?hl=cs#AuthScopes
   **/
  private final String AUTH_TOKEN_TYPE = "oauth2:http://picasaweb.google.com/data/";
  /** Type of accounts that we are interested in **/
  private final String ACCOUNT_TYPE = "com.google";
  /** Keys for a settings items - not auth-related stuff ;-) **/
  static final String PREFKEY_ACCOUNT_NAME = "pref1_name";
  static final String PREFKEY_AUTH_TOKEN = "pref2_token";
  private SharedPreferences settings;
  private AccountManager aManager;
  private String accountName;
  private String authToken;
  private HttpURLConnection connection;


  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Inspect preferences for saved values and get AccountManager
    settings = getPreferences(Activity.MODE_PRIVATE);
    // accountName = settings.getString(PREFKEY_ACCOUNT_NAME, null);
    // authToken = settings.getString(PREFKEY_AUTH_TOKEN, null);
    aManager = AccountManager.get(this);
    dance();
  }

  /**
   * Do the authentication dance.
   * At first try to use an account name and an authToken from the
   * SharedPreferences
   * If the first fails, try to get authToken for an accountName from
   * SharedPreferences
   * If the second fails, try to choose an account and get authToken for it.
   */
  public void dance() {
    Log.v(LOGTAG, "dance()");
    // We have got everything
    if (accountName != null && authToken != null) {
      listAlbums(true);
    } else {
      // We have to do more to obtain an authToken
      Account account = null;
      // We have got some account name, let's verify if it is still valid
      if (accountName != null) {
        Account[] accounts = aManager.getAccountsByType(ACCOUNT_TYPE);
        for (Account acc : accounts) {
          if (acc.name.equals(accountName)) {
            account = acc;
            break;
          }
        }
      }
      Bundle options=new Bundle();
      if (account != null) {
        // Well, we don't have an authToken, but we have a valid account
        aManager.getAuthToken(account, AUTH_TOKEN_TYPE, options, this, this,
            null);
      } else {
        // Hmmm, we have to choose an account to use
        Log.v(LOGTAG, "chooseAccount()");
        aManager.getAuthTokenByFeatures(ACCOUNT_TYPE, AUTH_TOKEN_TYPE, null,
            this, options, null, this, null);
      }
    }
  }
  /**
   * Callback method from getAuthTokenByFeatures() and getAuthToken(). All we
   * have to do
   * is remember an authToken and an accountName and then begin to work.
   * 
   * @param future
   */
  @Override
  public void run(AccountManagerFuture<Bundle> future) {
    Log.v(LOGTAG, "AccountManagerCallback.run()");
    try {
      Bundle bundle = future.getResult();
      if (bundle.containsKey(AccountManager.KEY_AUTHTOKEN)) {
        SharedPreferences.Editor editor = settings.edit();
        authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
        Log.v(LOGTAG, "setAuthToken()" + authToken);
        editor.putString(PREFKEY_AUTH_TOKEN, authToken);
        accountName = bundle.getString(AccountManager.KEY_ACCOUNT_NAME);
        Log.v(LOGTAG, "setAccountName()" + accountName);
        editor.putString(PREFKEY_ACCOUNT_NAME, accountName);
        editor.commit();
        listAlbums(true);
      }
    } catch (AuthenticatorException _e) {
      _e.printStackTrace();
      // TODO if the authenticator failed to respond
    } catch (OperationCanceledException _e) {
      _e.printStackTrace();
      // TODO user cancelled the operation
    } catch (IOException _e) {
      _e.printStackTrace();
      // TODO if I/O problem creating a new auth token, usually because of
      // network trouble
    }
  }

  public void listAlbums(boolean _ready) {
    URL url = null;
    try {
      url = new URL("https://picasaweb.google.com/data/feed/api/user/default");
    } catch (MalformedURLException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    try {
      connection = (HttpURLConnection) url.openConnection();
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    connection.addRequestProperty("X-GData-Client", CLIENT_ID);//This may be is not necessary
    connection.setRequestProperty("GData-Version", "2");
    connection.setRequestProperty("Authorization", "OAuth " + authToken);
    try {
      Log.v(LOGTAG, "Response" + connection.getResponseCode() + " ("
          + connection.getResponseMessage() + ")");
      if(connection.getResponseCode()==401 || connection.getResponseCode()==403){
        aManager.invalidateAuthToken(ACCOUNT_TYPE, authToken);
        authToken=null;
        //Dance again to refresh the authToken
        //- it should be treated more carefully to prevent an eternal dance
        dance();
        return;
      }
      //List user's albums
      DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
      try {
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
        Document doc = dBuilder.parse(connection.getInputStream());
        Element root = doc.getDocumentElement();
        NodeList nodes = root.getElementsByTagName("entry");
        for (int i = 0; i < nodes.getLength(); i++) {
          Element element = (Element)nodes.item(i);
          Element title=(Element)element.getElementsByTagName("title").item(0);
          Log.v(LOGTAG, title.getChildNodes().item(0).getNodeValue()+"");
        }
        
      } catch (ParserConfigurationException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      } catch (SAXException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }

    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
}

Note, that you will need following permissions declared in yout manifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >

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

Hope this will work for you.

Monday, September 3, 2012

Exif metadata handler for Android (pure JAVA)

In this article I will introduce you a single, pure JAVA one-class-based Exif metadata driver, Which allows you to read or modify any information in Exif area of JPEG file.

If you are trying to work with Exif metadata of JPEG images,  you probably know that Android provides native ExifInterface. Sadly this class does not allow you to modify some tags, namely Copyright, Artist, User description, e.t.c. So I have decided to write my own simple Exif driver, which allows me to do anything I need to do with the Exif data.

The work with the Exif driver looks like this:
exifDriver = ExifDriver.getInstance(originalFile.getAbsolutePath());
if (exifDriver != null) {
        exifManager = new ExifManager(exifDriver, mContext);
        // We can modify some info
        exifManager.setArtist("The artist");
        exifManager.setUserComment("User comment ");
        // Save the image - do not use the same one !
        exifDriver.save(modifiedFile.getAbsolutePath());
        // Here we go - read info from the modified Exif file
        exifDriver = ExifDriver.getInstance(modifiedFile.getAbsolutePath());
        if (exifDriver != null) {
          exifManager = new ExifManager(exifDriver, mContext);
          logTag(exifManager.getExifRelated(ExifDriver.TAG_USER_COMMENT));

The complete code of an example activity follows. The activity let's user take image with camera, modifies Artist and User comment tags and then displays all tags, which are set in image. Code looks long, but it is almost because the number of different tags is so high.
public class MainActivity extends Activity {
  private final String LOGTAG = getClass().getName();
  private final int REQUEST_FROM_CAMERA = 1;
  private ProgressDialog progressDialog;
  private File origFile;
  private File modifiedFile;
  private Uri photoUri;
  private ImageView imagePreview;
  private Context mContext;

  /**
   * Usual onCreate - nothing special there
   */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mContext = this;
    setContentView(R.layout.activity_main);
    imagePreview = (ImageView) findViewById(R.id.imagePreview);
  }

  /**
   * Convenience method - test if there is any camera app in the device
   * 
   * @param intent
   * @return
   */
  private boolean isIntentAvailable(Intent intent) {
    final PackageManager mgr = getPackageManager();
    return mgr.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
        .size() > 0;
  }

  /**
   * Button handler. Create intent for camera, create temp file and run intent
   */
  public void handleCamera(View _view) {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (isIntentAvailable(intent)) {
      String storageState = Environment.getExternalStorageState();
      if (storageState.equals(Environment.MEDIA_MOUNTED)) {
        GregorianCalendar today = new GregorianCalendar();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
        origFile = new File(Environment.getExternalStorageDirectory()
            + File.separator + getText(R.string.app_name) + File.separator
            + format.format(today.getTime()) + ".jpg");
        modifiedFile = new File(Environment.getExternalStorageDirectory()
            + File.separator + getText(R.string.app_name) + File.separator
            + format.format(today.getTime()) + "_modif.jpg");
        try {
          if (origFile.exists()) {
            origFile.delete();
          }
          origFile.getParentFile().mkdirs();
          origFile.createNewFile();

        } catch (IOException e) {
          Log.e(LOGTAG, "Could not create file.", e);
        }
        photoUri = Uri.fromFile(origFile);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
        startActivityForResult(intent, REQUEST_FROM_CAMERA);
      } else {
        new AlertDialog.Builder(this)
            .setMessage(
                "External Storeage (SD Card) is required.\n\nCurrent state: "
                    + storageState).setCancelable(true).create().show();
      }
    } else {
      new AlertDialog.Builder(this).setTitle("No camera")
          .setMessage("There is no camera application to take the picture")
          .setPositiveButton("OK", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface arg0, int arg1) {
            }
          }).show();
    }
  }

  /**
   * Get result from camera
   */
  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent _intent) {
    if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_FROM_CAMERA) {
      ImageDisplayer displayer = new ImageDisplayer();
      displayer.execute(origFile);
    }
  }

  /**
   * This is it.
   * 
   */
  private class ImageDisplayer extends AsyncTask<File, Drawable, Drawable> {
    @Override
    protected void onPreExecute() {
      progressDialog = ProgressDialog.show(mContext, "Loading", "");
    }

    private void logTag(String[] _keyValue) {
      if (_keyValue != null && _keyValue[0] != null && _keyValue[1] != null){
      Log.v(LOGTAG, _keyValue[0] + ": " + _keyValue[1]);
      }
    }

    @Override
    protected Drawable doInBackground(File... _path) {
      ExifManager exifManager;
      ExifDriver exifDriver = ExifDriver
          .getInstance(_path[0].getAbsolutePath());
      if (exifDriver != null) {
        exifManager = new ExifManager(exifDriver, mContext);
        // We can modify some info
        exifManager.setArtist("The artist");
        exifManager.setUserComment("User comment ");
        // Save the image - do not use the same one !
        exifDriver.save(modifiedFile.getAbsolutePath());
        // Here we go - read info from the modified Exif file
        exifDriver = ExifDriver.getInstance(modifiedFile.getAbsolutePath());
        if (exifDriver != null) {
          exifManager = new ExifManager(exifDriver, mContext);
          logTag(exifManager.getExifRelated(ExifDriver.TAG_USER_COMMENT));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_IMAGE_WIDTH));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_IMAGE_HEIGHT));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_BITS_PER_SAMPLE));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_COMPRESSION));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_PHOTOMETRIC_INTERPRETATION));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_ORIENTATION));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_SAMPLES_PER_PIXEL));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_PLANAR_CONFIGURATION));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_YCBCR_SUBSAMPLING));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_YCBCRPOSITIONING));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_XRESOLUTION));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_YRESOLUTION));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_RESOLUTION_UNIT));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_STRIP_OFFSETS));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_ROWS_PER_STRIP));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_STRIP_BYTECOUNTS));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_TRANSFER_FUNCTION));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_WHITE_POINT));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_PRIMARY_CHROMATICITIES));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_YCBCR_COEFICIENTS));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_REFERENCE_BLACK_WHITE));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_DATETIME));
          logTag(exifManager
              .getMainImageRelated(ExifDriver.TAG_IMAGE_DESCRIPTION));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_MAKE));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_MODEL));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_SOFTWARE));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_ARTIST));
          logTag(exifManager.getMainImageRelated(ExifDriver.TAG_COPYRIGHT));
          // IFD Exif tags
          logTag(exifManager.getExifRelated(ExifDriver.TAG_EXIF_VERSION));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_FLASHPIX_VERSION));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_COLOR_SPACE));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_COMPONENT_CONFIGURATION));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_COMPRESSED_BITS_PER_PIXEL));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_PIXEL_X_DIMENSION));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_PIXEL_Y_DIMENSION));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_MARKER_NOTE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_USER_COMMENT));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_RELATED_SOUND_FILE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_DATETIME_ORIGINAL));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_DATETIME_DIGITIZED));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_SUB_SEC_TIME));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_SUB_SEC_TIME_ORIGINAL));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_SUB_SEC_TIME_DIGITIZED));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_IMAGE_UNIQUE_ID));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_EXPOSURE_TIME));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_FNUMBER));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_EXPOSURE_PROGRAM));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_SPECTRAL_SENSITIVITY));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_ISO_SPEED_RATINGS));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_OECF));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_SHUTTER_SPEED_VALUE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_APERTURE_VALUE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_BRIGHTNESS_VALUE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_EXPOSURE_BIAS_VALUE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_MAX_APERTURE_VALUE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_SUBJECT_DISTANCE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_METERING_MODE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_LIGHT_SOURCE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_FLASH));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_FOCAL_LENGTH));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_SUBJECT_AREA));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_FLASH_ENERGY));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_SPATIAL_FREQUENCY_RESPONSE));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_FOCAL_PLANE_X_RESOLUTION));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_FOCAL_PLANE_Y_RESOLUTION));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_FOCAL_PLANE_RESOLUTION_UNIT));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_SUBJECT_LOCATION));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_EXPOSURE_INDEX));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_SENSING_METHOD));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_FILE_SOURCE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_SCENE_TYPE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_CFA_PATTERN));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_CUSTOM_RENDERED));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_EXPOSURE_MODE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_WHITE_BALANCE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_DIGITAL_ZOOM_RATIO));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_FOCAL_LENGTH_35MM_FILM));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_SCENE_CAPTURE_TYPE));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_GAIN_CONTROL));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_CONTRAST));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_SATURATION));
          logTag(exifManager.getExifRelated(ExifDriver.TAG_SHARPNESS));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_DEVICE_SETTING_DESCRIPTION));
          logTag(exifManager
              .getExifRelated(ExifDriver.TAG_SUBJECT_DISTANCE_RANGE));
          // Thumbnail tags
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_IMAGE_WIDTH));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_IMAGE_HEIGHT));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_JPEG_INTERCHANGE_FORMAT));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_BITS_PER_SAMPLE));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_COMPRESSION));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_PHOTOMETRIC_INTERPRETATION));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_ORIENTATION));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_SAMPLES_PER_PIXEL));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_PLANAR_CONFIGURATION));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_YCBCR_SUBSAMPLING));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_YCBCRPOSITIONING));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_XRESOLUTION));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_YRESOLUTION));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_RESOLUTION_UNIT));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_STRIP_OFFSETS));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_ROWS_PER_STRIP));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_STRIP_BYTECOUNTS));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_TRANSFER_FUNCTION));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_WHITE_POINT));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_PRIMARY_CHROMATICITIES));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_YCBCR_COEFICIENTS));
          logTag(exifManager
              .getThumbnailRelated(ExifDriver.TAG_REFERENCE_BLACK_WHITE));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_DATETIME));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_MAKE));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_MODEL));
          logTag(exifManager.getThumbnailRelated(ExifDriver.TAG_SOFTWARE));
          // IFD GPS tags
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_VERSION_ID));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_LATITUDE_REF));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_LATITUDE));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_LONGITUDE_REF));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_LONGITUDE));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_ALTITUDE_REF));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_ALTITUDE));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_TIME_STAMP));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_SATELITES));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_STATUS));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_MEASURE_MODE));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_DOP));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_SPEED_REF));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_SPEED));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_TRACK_REF));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_TRACK));
          logTag(exifManager
              .getGpsRelated(ExifDriver.TAG_GPS_SLMG_DIRECTION_REF));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_SLMG_DIRECTION));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_MAP_DATUM));
          logTag(exifManager
              .getGpsRelated(ExifDriver.TAG_GPS_DEST_LATITUDE_REF));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_DEST_LATITUDE));
          logTag(exifManager
              .getGpsRelated(ExifDriver.TAG_GPS_DEST_LONGITUDE_REF));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_DEST_LONGITUDE));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_DEST_BEARING_REF));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_DEST_BEARING));
          logTag(exifManager
              .getGpsRelated(ExifDriver.TAG_GPS_DEST_DISTANCE_REF));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_DEST_DISTANCE));
          logTag(exifManager
              .getGpsRelated(ExifDriver.TAG_GPS_PROCESSING_METHOD));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_AREA_INFORMATION));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_DATE_STAMP));
          logTag(exifManager.getGpsRelated(ExifDriver.TAG_GPS_DIFFERENTIAL));
        } else {
          Log.e(LOGTAG,
              "Could not read exif info from the modified image - and this is bad");
        }
      } else {
        Log.e(LOGTAG, "Could not read exif info from an original image");
      }
      return Drawable.createFromPath(_path[0].getAbsolutePath());
    }

    @Override
    protected void onPostExecute(Drawable _drawable) {
      imagePreview.setImageDrawable(_drawable);
      progressDialog.dismiss();
    }
  }
}

You can look at the code of the driver here:
https://code.google.com/p/exif-driver/source/browse/src/rcs34/android/libs/ExifDriver/
or you can cone the code from git repository
git clone https://code.google.com/p/exif-driver/

The driver tries to conform with Exif Version 2.2. For more info see http://exif.org/ . It can handle both - pure Exif images and JFIF images with included Exif data. Of course there is a relatively high probability of bugs, because the class is quite young and not well tested. You are welcome to send me images, which the ExifDriver will not work with.

 Hope this will work for you.

Sunday, September 2, 2012

Load local and remote image from gallery by Intent

In this article you will learn, how images from gallery can be load into your activity no matter if they are local or stored remotely (i.e. in Picasa).

If you are interested in how to load image from gallery to your activity, you almost certainly know, that you have to use Intent.ACTION_PICK and startActivityForResult. You probably know, that the result intent, which you will obtain by onActivityResult contains Uri of the selected image and you can get the Uri by the getData method. Maybe you have learned, that Uri got by the getData is not Uri of actual file, but only a kind of "database key".

Commonly used (wrong) approach uses ContentResolver query, which is supposed to return cursor, that holds the image file path in the MediaStore.Images.Media.DATA column.

This method will work well for local files. For files stored remotely, i.e. in Picasa gallery this method will work only on some (older) versions of Android. I have learned, that my Android 2.3 on my LG P350 downloads remote images which I select in gallery to Downloads folder, so they become local. But Android 3.2 on my PackardBell tablet does not do any download automatically and therefore above mentioned approach fails and I get Null pointer error when I try to work with remotely stored images.

Following code shows another approach, which seems to work with both local and remote images well. It uses ContentProviderClient and ParcelFileDescriptor. It loads Image to temp file and works with this local copy. Note, thet _path[0] holds Uri got by getData method from the result Intent.
@Override
    protected Drawable doInBackground(Uri... _path) {
      file = null;
      // This is the key line. Content provider client gives us access to
      // file no matter if it is a local or a remote one
      ContentProviderClient client = getContentResolver()
          .acquireContentProviderClient(_path[0]);
      try {
        // Here we save copy of the file to temporary
        ParcelFileDescriptor descriptor = client.openFile(_path[0], "r");
        AutoCloseInputStream is = new AutoCloseInputStream(descriptor);
        file = File.createTempFile("image", ".jpg", getDir(null, MODE_PRIVATE));
        OutputStream outS = new FileOutputStream(file);
        byte[] buf = new byte[1024];
        int len = 0;
        while ((len = is.read(buf)) > 0) {
          outS.write(buf, 0, len);
        }
        is.close();
        outS.close();
Complete code of test activity follows. To make it work, you have to create activity_galery_get.xml which will provide ImageView and Button.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/previewFrame"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/imagePreview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:contentDescription="Image to share" >
        </ImageView>
        
        <Button
            android:id="@+id/bFromGallery"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="left"
            android:onClick="handleFromGallery"
            android:text="Select image from gallery" />
        
    </FrameLayout>

Here is the complete activity code:
public class GaleryGet extends Activity {
  private final String LOGTAG = getClass().getName();
  private final int REQUEST_IMAGE = 33;
  private ProgressDialog progressDialog;
  private File file = null;
  private ImageView imagePreview;
  private Context mContext;

  /**
   * Usual onCreate - nothing special there
   */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mContext=this;
    setContentView(R.layout.activity_galery_get);
    imagePreview = (ImageView) findViewById(R.id.imagePreview);
  }

  /**
   * Button handler. Also very common one. Just create Intent and run it.
   */
  public void handleFromGallery(View _view) {
    Intent i = new Intent(Intent.ACTION_PICK,
        android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
    startActivityForResult(i, REQUEST_IMAGE);
  }

  /**
   * Get result from gallery. Well we extract URI from intent and run
   * ImageDisplayer
   */
  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent _intent) {
    if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMAGE) {
      ImageDisplayer displayer = new ImageDisplayer();
      if(_intent.getData()!=null){
      displayer.execute(_intent.getData());
      }
    }
  }

  /**
   * This is it. here we can see, how to process Uri from gallery result
   * in a proper way to get image displayed.
   * 
   */
  private class ImageDisplayer extends AsyncTask<Uri, Drawable,Drawable> {
    @Override
    protected void onPreExecute(){
      progressDialog = ProgressDialog.show(mContext, "Loading", "");
    }
    @Override
    protected Drawable doInBackground(Uri... _path) {
      file = null;
      // This is the key line. Content provider client gives us access to
      // file no matter if it is a local or a remote one
      ContentProviderClient client = getContentResolver()
          .acquireContentProviderClient(_path[0]);
      try {
        // Here we save copy of the file to temporary
        ParcelFileDescriptor descriptor = client.openFile(_path[0], "r");
        AutoCloseInputStream is = new AutoCloseInputStream(descriptor);
        file = File.createTempFile("image", ".jpg", getDir(null, MODE_PRIVATE));
        OutputStream outS = new FileOutputStream(file);
        byte[] buf = new byte[1024];
        int len = 0;
        while ((len = is.read(buf)) > 0) {
          outS.write(buf, 0, len);
        }
        is.close();
        outS.close();
      } catch (RemoteException e1) {
        // TODO Auto-generated catch block
        e1.printStackTrace();
      } catch (FileNotFoundException e1) {
        // TODO Auto-generated catch block
        e1.printStackTrace();
      } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
      // And finally, we display the image
      String filePath = file.getAbsolutePath();
      return Drawable.createFromPath(filePath);
    }

    @Override
    protected void onPostExecute(Drawable _drawable) {
      imagePreview.setImageDrawable(_drawable);
      progressDialog.dismiss();
    }
  }
}
Hope this will work for you.

Wednesday, August 29, 2012

Detect onDoubleTap or onLongPress on a MapView

In this article you will learn how a long click or a double click can be detected on a MapView object.

If your application uses a MapView from Google APIs, you could be interested in how to detect some simple gestures like a double tap (double click) or a long tap (long click).

As you probably know already, you have to use ItemizedOverlay subclass to work with tap events on a MapView.  Sadly ItemizedOverlay provides only onTouchEvent(MotionEvent) and onTap(int), which can be a little bit challenging to use for detecting more complex events.

Fortunately, the solution is quite simple as we can use GestureDetector and SimpleOnGestureListener. Following code sample shows, how to do it quite simply:

  // Overlay
  private class Overlays extends ItemizedOverlay {
    private ArrayList<overlayitem> overlays = new ArrayList<overlayitem>();

    private GestureDetector gestureDetector;

    //Private MapGestureListener which will listen to just
    //a long and a double press
    private class MapGestureListener extends SimpleOnGestureListener {
      @override
      public void onLongPress(MotionEvent _motion) {
        //Do something nice here
      }

      @override
      public boolean onDoubleTap(MotionEvent _motion) {
      //Do something nice here
       return true;
      }
    }
    //ItemizedOverlay constructor. Register GestureDetector and 
    //MapGestureListener right here
    public Overlays(Drawable defaultMarker) {
      super(boundCenterBottom(defaultMarker));
      gestureDetector = new GestureDetector(GApp.context,
          new MapGestureListener());
    }
    //onTap() method should work normally
    @Override
    public boolean onTap(int _index) {
      Toast.makeText(GApp.context, overlays.get(_index).getTitle(),
          Toast.LENGTH_SHORT).show();
      return true;
    }
    //Ship onTouchEvent to our GestureDetector and return what it returns
    @Override
    public boolean onTouchEvent(MotionEvent _motion, MapView _mapView) {
      return gestureDetector.onTouchEvent(_motion);
    }
  }

Hope this will work for you.