Sunday 23 November 2014

Android, SQLite: EXPLAIN QUERY PLAN method for UPDATE queries

If you're using the SQLiteDatabase.update(String table, ContentValues values, String whereClause, String[] whereArgs) method for updating records in your local database, here are a couple of helper methods to compose and execute EXPLAIN QUERY PLAN commands for analysing your UPDATE queries:

/**
 * Composes and executes an EXECUTE QUERY PLAN command
 * for the UPDATE query that would be composed from the parameters provided.
 * 
 * @see {@link SQLiteDatabase#update(String, ContentValues, String, String[])} for a description of this method's parameters.
 */
private static void explainQueryPlanForUpdateStatement(SQLiteDatabase database, String table, ContentValues contentValues, String selection, String[] selectionArgs) {

  final StringBuilder sb = new StringBuilder();
  sb.append("EXPLAIN QUERY PLAN UPDATE ");

  sb.append(table);

  sb.append(" SET ");

  final Set keys = contentValues.keySet();

  boolean firstKey = true;

  for (String key : keys) {
    if (!firstKey) {
      sb.append(", ");
    }

    sb.append(key);
    sb.append(" = ");

    if (contentValues.get(key) == null) {
      sb.append("NULL");
    } else if (contentValues.get(key) instanceof Boolean) {
      Boolean value = (Boolean) contentValues.get(key);

      if (value.booleanValue()) {
        sb.append("1");
      } else {
        sb.append("0");
      }
    } else if (contentValues.get(key) instanceof Number) {
      sb.append(contentValues.get(key).toString());
    } else {
      sb.append("'");
      sb.append(contentValues.get(key).toString());
      sb.append("' ");
    }

    firstKey = false;
  }

  if (!TextUtils.isEmpty(selection)) {
    sb.append(" WHERE ");
    sb.append(selection);
  }

  executeExplainQueryPlanStatement(database, sb.toString(), selectionArgs);

}

/**
 * Executes sql using database
 * and prints the result to logs.
 * 
 * @param database the {@link SQLiteDatabase} instance to use to execute the query.
 * @param sql is an EXPLAIN QUERY PLAN command which must not be ; terminated.
 * @param selectionArgs the values to replace the ?s in the where clause of sql.
 */
private static void executeExplainQueryPlanStatement(SQLiteDatabase database, String sql, String[] selectionArgs) {

  final Cursor cursor = database.rawQuery(sql, selectionArgs);

  if (cursor.moveToFirst()) {
    final int colIndexSelectId = cursor.getColumnIndex("selectid");
    final int colIndexOrder = cursor.getColumnIndex("order");
    final int colIndexFrom = cursor.getColumnIndex("from");
    final int colIndexDetail = cursor.getColumnIndex("detail");

    final int selectId = cursor.getInt(colIndexSelectId);
    final int order = cursor.getInt(colIndexOrder);
    final int from = cursor.getInt(colIndexFrom);
    final String detail = cursor.getString(colIndexDetail);

    Log.d(TAG, sql);
    Log.d(TAG, String.format("%d | %d | %d | %s", selectId, order, from, detail));
  }

  cursor.close();

}
UPDATE: These methods are now contained in the QueryPlanExplainer class in this repository.

Saturday 22 November 2014

Android, SQLite: EXPLAIN QUERY PLAN method for SELECT queries

If you're using the SQLiteDatabase.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) method for performing your local database queries (or one of the similar SQLiteDatabase query methods), here are a couple of helper methods to compose and execute EXPLAIN QUERY PLAN commands for analysing your SELECT queries:

/**
 * Composes and executes an EXECUTE QUERY PLAN command
 * for the SELECT query that would be composed from the parameters provided.
 * 
 * @see {@link SQLiteDatabase#query(String, String[], String, String[], String, String, String, String)} for a description of this method's parameters.
 */
private static void explainQueryPlanForSelectStatement(SQLiteDatabase database, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) {

  final StringBuilder sb = new StringBuilder();
  sb.append("EXPLAIN QUERY PLAN SELECT ");

  if (columns == null || columns.length == 0) {
    sb.append(" * ");
  } else {
    boolean firstColumn = true;

    for (String column : columns) {
      if (!firstColumn) {
        sb.append(", ");
      }

      sb.append(column);

      firstColumn = false;
    }
  }

  sb.append(" FROM ");
  sb.append(table);

  if (!TextUtils.isEmpty(selection)) {
    sb.append(" WHERE ");
    sb.append(selection);
  }

  if (!TextUtils.isEmpty(groupBy)) {
    sb.append(" GROUP BY ");
    sb.append(groupBy);
  }

  if (!TextUtils.isEmpty(having)) {
    sb.append(" HAVING ");
    sb.append(having);
  }

  if (!TextUtils.isEmpty(orderBy)) {
    sb.append(" ORDER BY ");
    sb.append(orderBy);
  }

  if (!TextUtils.isEmpty(limit)) {
    sb.append(" LIMIT ");
    sb.append(limit);
  }

  executeExplainQueryPlanStatement(database, sb.toString(), selectionArgs);

}

/**
 * Executes sql using database
 * and prints the result to logs.
 * 
 * @param database the {@link SQLiteDatabase} instance to use to execute the query.
 * @param sql is an EXPLAIN QUERY PLAN command which must not be ; terminated.
 * @param selectionArgs the values to replace the ?s in the where clause of sql.
 */
private static void executeExplainQueryPlanStatement(SQLiteDatabase database, String sql, String[] selectionArgs) {

  final Cursor cursor = database.rawQuery(sql, selectionArgs);

  if (cursor.moveToFirst()) {
    final int colIndexSelectId = cursor.getColumnIndex("selectid");
    final int colIndexOrder = cursor.getColumnIndex("order");
    final int colIndexFrom = cursor.getColumnIndex("from");
    final int colIndexDetail = cursor.getColumnIndex("detail");

    final int selectId = cursor.getInt(colIndexSelectId);
    final int order = cursor.getInt(colIndexOrder);
    final int from = cursor.getInt(colIndexFrom);
    final String detail = cursor.getString(colIndexDetail);

    Log.d(TAG, sql);
    Log.d(TAG, String.format("%d | %d | %d | %s", selectId, order, from, detail));
  }

  cursor.close();

}

UPDATE: These methods are now contained in the QueryPlanExplainer class in this repository.

Sunday 21 September 2014

Android: a ListView for which scrolling can be disabled

Given a parent View which contains other child Views, the way touch generally works in Android is that the parent View will only see a touch event if it's not consumed by one of its child Views, i.e. the child Views have first say on whether they want to act on a touch event. This is true in most circumstances but there are some exceptions. Notably, it is possible for a parent View to intercept touch events so that it processes them instead of the child Views. An example of this is the ListView class: when the user moves his/her finger up or down in the ListView, the touch events are intercepted by the ListView so that it scrolls vertically and the child Views do not see the touch events. If you'd like to stop the ListView stealing the touch events, here's how: instantiate the custom ListView class below and set scrollEnabled to false:

public class DisableScrollListView extends ListView {

  /**
   * Flag which determines whether vertical scrolling is enabled in this {@link ListView}.
   */
  private boolean scrollEnabled = true;

  public DisableScrollListView(Context context) {
    super(context);
  }

  public DisableScrollListView(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  public DisableScrollListView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
  }

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (!scrollEnabled) {
      return false;
    } else {
      return super.onInterceptTouchEvent(ev);
    }
  }

  /**
   * Sets the value of {@link #scrollEnabled}.
   * 
   * @param scrollEnabled
   */
  public void setScrollEnabled(boolean scrollEnabled) {
    this.scrollEnabled = scrollEnabled;
  }
}

Android: methods for converting pixels to scaled pixels, and vice versa

Here's a couple of methods for converting from pixels to scaled pixels (for use in TextViews to set the size of text), and vice versa.

If you need to convert between pixels and density-independent pixels, then you need these methods instead:
http://adilatwork.blogspot.co.uk/2011/09/android-methods-for-converting-density.html
/**  
  * @param scaledPixels  
  * @return the number of pixels which scaledPixels corresponds to  
  * on the device.  
  */  
 private float convertSpToPx(float scaledPixels) {  
  DisplayMetrics dm = getContext().getResources().getDisplayMetrics();  
  return scaledPixels * dm.scaledDensity;  
 }  
   
 /**  
  * @param pixels  
  * @return the number of scaled pixels which pixels corresponds to  
  * on the device.  
  */  
 private float convertPxToSp(float pixels) {  
  DisplayMetrics dm = getContext().getResources().getDisplayMetrics();  
  return pixels / dm.scaledDensity;  
 }

Sunday 10 August 2014

Android: TextView which resizes its text to fit within its height and width

Below is an extension of TextView which resizes text to fit within its height and width. It allows a maximum and minimum text size to be specified, and allows text to span multiple lines. It is a modification (some fixes and simplifications) of Chase's answer on Stack Overflow here: http://stackoverflow.com/a/5535672/1071320

UPDATE: For the latest version of the AutoResizeTextView class, see this repository:
https://bitbucket.org/adilson05uk/android-utils
public class AutoResizeTextView extends TextView {  
   
  /** Our ellipsis string. */  
  private static final String mEllipsis = "\u2026";  
   
  /**  
   * Upper bounds for text size.  
   * This acts as a starting point for resizing.  
   */  
  private float mMaxTextSizePixels;  
   
  /** Lower bounds for text size. */  
  private float mMinTextSizePixels;  
   
  /** TextView line spacing multiplier. */  
  private float mLineSpacingMultiplier = 1.0f;  
   
  /** TextView additional line spacing. */  
  private float mLineSpacingExtra = 0.0f;  
   
  /**  
   * Default constructor override.  
   *   
   * @param context  
   */  
  public AutoResizeTextView(Context context) {  
   this(context, null);
  }  
   
  /**  
   * Default constructor when inflating from XML file.  
   *   
   * @param context  
   * @param attrs  
   */  
  public AutoResizeTextView(Context context, AttributeSet attrs) {  
   this(context, attrs, 0);
  }  
   
  /**  
   * Default constructor override.  
   *   
   * @param context  
   * @param attrs  
   * @param defStyle  
   */  
  public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
   super(context, attrs, defStyle);
   initialise();  
  }  

  @Override  
  protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
   super.onLayout(changed, left, top, right, bottom);
   resizeText();
  }  

  @Override  
  protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
   super.onTextChanged(text, start, lengthBefore, lengthAfter);
   requestLayout();
  }  
   
  @Override  
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
   super.onSizeChanged(w, h, oldw, oldh);  
   
   if (w != oldw || h != oldh) {  
    requestLayout();  
   }  
  }  
   
  @Override  
  public void setTextSize(float size) {  
   setTextSize(TypedValue.COMPLEX_UNIT_SP, size);  
  }  
   
  @Override  
  public void setTextSize(int unit, float size) {  
   super.setTextSize(unit, size);  
   mMaxTextSizePixels = getTextSize();  
   requestLayout();  
  }  
   
  @Override  
  public void setLineSpacing(float add, float mult) {  
   super.setLineSpacing(add, mult);  
   mLineSpacingMultiplier = mult;  
   mLineSpacingExtra = add;  
   requestLayout();  
  }  
   
  @Override  
  public void setEllipsize(TruncateAt where) {  
   super.setEllipsize(where);  
   requestLayout();  
  }  
   
  /**  
   * Sets the lower text size limit and invalidates the view.  
   *   
   * @param minTextSizeScaledPixels the minimum size to use for text in this view,  
   * in scaled pixels.  
   */  
  public void setMinTextSize(float minTextSizeScaledPixels) {  
   mMinTextSizePixels = convertSpToPx(minTextSizeScaledPixels);  
   requestLayout();  
  }  
   
  /**  
   * @return lower text size limit, in pixels.  
   */  
  public float getMinTextSizePixels() {  
   return mMinTextSizePixels;  
  }  
   
  private void initialise() {  
   mMaxTextSizePixels = getTextSize();  
  }  
   
  /**  
   * Resizes this view's text size with respect to its width and height  
   * (minus padding).  
   */  
  private void resizeText() {  
   final int availableHeightPixels = getHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop();  
   
   final int availableWidthPixels = getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();  
   
   final CharSequence text = getText();  
   
   // Safety check  
   // (Do not resize if the view does not have dimensions or if there is no text)  
   if (text == null  
     || text.length() <= 0  
     || availableHeightPixels <= 0  
     || availableWidthPixels <= 0  
     || mMaxTextSizePixels <= 0) {  
    return;  
   }  
   
   float targetTextSizePixels = mMaxTextSizePixels;  
   int targetTextHeightPixels = getTextHeightPixels(text, availableWidthPixels, targetTextSizePixels);

   // Until we either fit within our TextView  
   // or we have reached our minimum text size,  
   // incrementally try smaller sizes  
   while (targetTextHeightPixels > availableHeightPixels  
     && targetTextSizePixels > mMinTextSizePixels) {  
    targetTextSizePixels = Math.max(  
      targetTextSizePixels - 2,  
      mMinTextSizePixels);  
   
    targetTextHeightPixels = getTextHeightPixels(  
      text,  
      availableWidthPixels,  
      targetTextSizePixels);  
   }  
   
   // If we have reached our minimum text size and the text still doesn't fit,  
   // append an ellipsis  
   // (NOTE: Auto-ellipsize doesn't work hence why we have to do it here)  
   // (TODO: put ellipsis at the beginning, middle or end  
   // depending on the value of getEllipsize())  
   if (getEllipsize() != null  
     && targetTextSizePixels == mMinTextSizePixels  
     && targetTextHeightPixels > availableHeightPixels) {  
    // Make a copy of the original TextPaint object for measuring  
    TextPaint textPaintCopy = new TextPaint(getPaint());  
    textPaintCopy.setTextSize(targetTextSizePixels);  
   
    // Measure using a StaticLayout instance  
    StaticLayout staticLayout = new StaticLayout(  
      text,  
      textPaintCopy,  
      availableWidthPixels,  
      Alignment.ALIGN_NORMAL,  
      mLineSpacingMultiplier,  
      mLineSpacingExtra,  
      false);  
   
    // Check that we have a least one line of rendered text  
    if (staticLayout.getLineCount() > 0) {  
     // Since the line at the specific vertical position would be cut off,  
     // we must trim up to the previous line and add an ellipsis  
     int lastLine = staticLayout.getLineForVertical(availableHeightPixels) - 1;  
   
     if (lastLine >= 0) {  
      int startOffset = staticLayout.getLineStart(lastLine);  
      int endOffset = staticLayout.getLineEnd(lastLine);  
      float lineWidthPixels = staticLayout.getLineWidth(lastLine);  
      float ellipseWidth = textPaintCopy.measureText(mEllipsis);  
   
      // Trim characters off until we have enough room to draw the ellipsis  
      while (availableWidthPixels < lineWidthPixels + ellipseWidth) {  
       endOffset--;  
       lineWidthPixels = textPaintCopy.measureText(  
         text.subSequence(startOffset, endOffset + 1).toString());  
      }  
   
      setText(text.subSequence(0, endOffset) + mEllipsis);  
     }  
    }  
   }  
   
   super.setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSizePixels);  
   // Some devices try to auto adjust line spacing, so force default line spacing  
   super.setLineSpacing(mLineSpacingExtra, mLineSpacingMultiplier);  
  }  
   
  /**  
   * Sets the text size of a clone of the view's {@link TextPaint} object  
   * and uses a {@link StaticLayout} instance to measure the height of the text.  
   *   
   * @param source  
   * @param textPaint  
   * @param availableWidthPixels  
   * @param textSizePixels  
   * @return the height of the text when placed in a view  
   * with the specified width  
   * and when the text has the specified size.  
   */  
  private int getTextHeightPixels(  
    CharSequence source,  
    int availableWidthPixels,  
    float textSizePixels) {  
   // Make a copy of the original TextPaint object  
   // since the object gets modified while measuring  
   // (see also the docs for TextView.getPaint()  
   // which states to access it read-only)  
   TextPaint textPaintCopy = new TextPaint(getPaint());  
   textPaintCopy.setTextSize(textSizePixels);  
   
   // Measure using a StaticLayout instance  
   StaticLayout staticLayout = new StaticLayout(  
     source,  
     textPaintCopy,  
     availableWidthPixels,  
     Alignment.ALIGN_NORMAL,  
     mLineSpacingMultiplier,  
     mLineSpacingExtra,  
     true);  
   
   return staticLayout.getHeight();  
  }  
   
  /**  
   * @param scaledPixels  
   * @return the number of pixels which scaledPixels corresponds to on the device.  
   */  
  private float convertSpToPx(float scaledPixels) {  
   float pixels = scaledPixels * getContext().getResources().getDisplayMetrics().scaledDensity;  
   return pixels;  
  }  
 }

Tuesday 20 May 2014

Why is programming fun?

"First is the sheer joy of making things...

Second is the pleasure of making things that are useful to other people...

Third is the fascination of fashioning complex puzzle-like objects of interlocking moving parts...

Fourth is the joy of always learning, which springs from the non-repeating nature of the task...

Finally, there is the joy of working in such a tractable medium..."

(Snippet from 'The Mythical Man-Month' by Frederick P. Brooks, Jr. [Chapter 1 - The Tar Pit])

Saturday 5 April 2014

Java - convert an integer to a binary (bits/bytes) String

So! You want to convert an integer to its binary (bits and bytes) representation. There is an existing method which does this:

Integer.toBinaryString(int);

But! This will not include the leading 0s (i.e. the 0s before the most-significant 1). If you want a String with all 32 bits that make up the integer, here's an alternative method:

/**
 * @param value
 * @return the binary representation of {@code value}, including leading 0s.
 */
String toBinaryString(int value) {
  final byte[] bytes = ByteBuffer
      .allocate(4)
      .order(ByteOrder.BIG_ENDIAN)
      .putInt(value)
      .array();

  return toBinaryString(bytes);
}

/**
 * @param bytes is non-null.
 * @return the binary representation of the bytes in {@code bytes}.
 */
String toBinaryString(byte[] bytes) {
  final StringBuilder sb = new StringBuilder();

  for (int i = 0; i < bytes.length; i++) {
    sb.append(toBinaryString(bytes[i]));
    sb.append(" ");
  }

  return sb.toString();
}

/**
 * @param value
 * @return the binary representation of {@code value}.
 */
String toBinaryString(byte value) {
  final StringBuilder sb = new StringBuilder();

  for (int i = 7; i >= 0; i--) {
    boolean bitSet = (value & (1 << i)) > 0;
    sb.append(bitSet ? "1" : "0");
  }

  return sb.toString();
}

Monday 10 March 2014

Android/Java - formatting a String containing both English and Arabic

I ran across a problem today where I needed to compose a String containing both English (left-to-right text) and Arabic (right-to-left text) parts. In particular, what I needed to do was format a String containing three blocks to display as follows:

[1. English left-to-right text] [2. Arabic right-to-left text] [3. English left-to-right text]

The problem I found was that some of the English in the third block was displayed to the left of the Arabic in the second block, i.e. the implicit text ordering algorithm wasn't able to correctly determine where the Arabic right-to-left block had completed and the next English left-to-right block had started.

To get around this problem I had to explicitly specify directional formatting characters (\u202A, \u202B, \u202C, \u202D, \u202E etc) something as follows:

[1. English left-to-right text] \u202B [2. Arabic right-to-left text] \u202C [3. English left-to-right text]

To get the text to render exactly as you intend you might need to play around a little with the directional formatting characters. For example, you might have to surround the English block with directional formatting characters as well as the Arabic text, as follows:

\u202D [1. English left-to-right text] \u202B [2. Arabic right-to-left text] \u202C [3. English left-to-right text] \u202C

Here's the Stack Overflow thread which led me to the solution:
http://stackoverflow.com/questions/6177294/string-concatenation-containing-arabic-and-western-characters

For a deeper understanding of directional formatting characters and the Unicode Bidirectional Algorithm, check out the following document:
https://unicode.org/reports/tr9

Monday 10 February 2014

Android - how to make a custom view which looks like a Spinner

I'll show you in this post how to make a custom view which looks like a Spinner and to which you can assign your own text and click listener. There's just two steps.

Step 1 - define an xml layout for your custom view as follows:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/view_root"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  style="?android:attr/spinnerStyle" >

  <include
    layout="@android:layout/simple_spinner_item"
    android:id="@+id/view_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

</FrameLayout>

We'll assume you've saved the above layout in res/layout/spinner_lookalike.xml.

Step 2 - create your custom view class as follows:

/**
 * This class is essentially a {@link FrameLayout}
 * which looks like a {@link Spinner}.
 */
public class SpinnerLookalikeView extends FrameLayout {

  private ViewGroup rootView;
  private TextView textView;

  /**
   * Constructor to use when creating View from code.
   * */
  public SpinnerLookalikeView(Context context) {
    super(context);
    initialise();
  }

  /**
   * Constructor that is used when inflating View from XML.
   * */
  public SpinnerLookalikeView(Context context, AttributeSet attrs) {
    super(context, attrs);
      initialise();
  }

  /**
   * Constructor that is used when inflating View from XML
   * and applying a class-specific base style.
   * */
  public SpinnerLookalikeView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initialise();
  }

  @Override
  public void setOnClickListener(OnClickListener listener) {
    rootView.setOnClickListener(listener);
  }

  /**
   * Sets the text shown in this view.
   * 
   * @param text
   */
  public void setText(String text) {
    textView.setText(text);
  }

  /**
   * Initialisation method to be called by the constructors of this class only.
   */
  private void initialise() {
    LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    inflater.inflate(R.layout.spinner_lookalike, this);

    rootView = (ViewGroup) findViewById(R.id.view_root);
    textView = (TextView) findViewById(R.id.view_text);
  }

}

That's it! Now you can add this custom view to your activities and assign text and a click listener to it.

Sunday 26 January 2014

Android: NumberPicker - calling setWrapSelectorWheel(false) does nothing!!

In case you're using a NumberPicker instance, you have a good range of min-max values and you're calling NumberPicker.setWrapSelectorWheel(false) but it isn't disabling wrapping around the min/max values, it could be a simple issue of re-ordering your method calls. So, this won't work...

numberPicker.setWrapSelectorWheel(false);
numberPicker.setMinValue(0);
numberPicker.setMaxValue(10);
numberPicker.setValue(5);

... but this will...

numberPicker.setMinValue(0);
numberPicker.setMaxValue(10);
numberPicker.setValue(5);
numberPicker.setWrapSelectorWheel(false);

The key is to call NumberPicker.setWrapSelectorWheel(false) after you've set your NumberPicker's min and max values.