CursorJoinerを使ってMediaStore.Images.Thumbnailsに回転情報を追加する


ギャラリーから画像をまとめて表示したい場合、ContentResolverを使って画像のThumbnails情報を取ってきて表示するのが一般的かと思います。
が、注意すべき点として、Thumbnails画像には回転情報が含まれていないため、端末の機種によっては縦横の表示がおかしくなるケースがあります。
具体的にはGalaxy S4以降では縦長画像が横向きになってしまう現象が確認されました。

##解決方法

ContentResolverの `MediaStore.Images.Media._ID` と `MediaStore.Images.Thumbnails.IMAGE_ID`が同じことを利用し、MediaStore.Images.Mediaの回転情報を取得します。表示する際はこの回転情報を元にbitmapを回転して表示してあげます。PicassoやGlideを使えば画像回転をよろしくやってくれる(と、いうかPicassoでは内部的にMediaStore.Imagesの回転情報を取得する機構があるっぽい)かもしれませんが、今回は割愛します。

### CursorJoinerをカスタマイズする
一致する`MediaStore.Images.Media._ID` と `MediaStore.Images.Thumbnails.IMAGE_ID`を抽出するにはCursorJoinerを使います。  
CursorJoinerはその名の通りCursorをjoinします。内部的には２つのCursorを比較し、一致する項目が出てくるまでそれぞれのCursorを順に進めていくようになっています。ただしString型でしか比較してくれません。そのため、IDのint型でも対応できるようにCursorJoinerをカスタマイズします。実装方法は[ここのブログ](http://poly.hatenablog.com/entry/20101006/p1)(なんと5年前！)にも書いてあるのですが、写真の一覧の場合新しいものを先頭に持ってきたいので降順(DESC)で比較できるように工夫します。
実際の実装は[Gist](https://gist.github.com/tomoima525/59298f6aee6bc0ef5e9e00f992101552)に貼っています。

変更点をかいつまんで書くと  
(1)型と比較順序に関するenum追加

```
/**
 * The type of column to join
 */
public enum JoinColumnType {
    STRING,
    INT
}
/**
 * Order
 */
public enum OrderType {
    ASC, DESC;
}

//コンストラクタ内
        mType = type;
        switch (mType) {
            case STRING:
                mStringValues = new String[mColumnsLeft.length * 2];
                break;
            case INT:
                mIntValues = new int[mColumnsLeft.length * 2];
        }
        
        sOrderType = orderType;
```



(2) 型によって比較方法を分岐させる
CursorJoinerはIteratorになっています。next()メソッドで順次Cursorを移動させて比較するわけですが、型によって処理を分岐させます。


```
public Result next() {
...
　　if (hasLeft && hasRight) {
　　    int compareResult = 0;
　　    switch (mType){
　　        case STRING:
　　            populateStringValues(mStringValues, mCursorLeft, mColumnsLeft, 0);
　　            populateStringValues(mStringValues, mCursorRight, mColumnsRight, 1);
　　            compareResult = compareStrings(mStringValues);
　　            break;
　　        case INT:
　　            populateIntValues(mIntValues, mCursorLeft, mColumnsLeft, 0);
　　            populateIntValues(mIntValues, mCursorRight, mColumnsRight, 1);
　　            compareResult = compareInts(mIntValues);
　　            break;
```

int型の比較(`compareInts()`)は以下のようになります。OrderTypeによって返る値が変わる(どちらのCursorを進めるか決める)ことがわかります。

```java
private static int compareInts(int... values) {
    if ((values.length % 2) != 0) {
        throw new IllegalArgumentException("you must specify an even number of values");
    }

    for (int index = 0; index < values.length; index += 2) {
        int comp = values[index] - values[index + 1];
        if (comp != 0) {
            if (sOrderType == OrderType.DESC) {
                // if LEFT is smaller than RIGHT, move RIGHT cursor since value order is desc
                return comp < 0 ? 1 : -1;
            } else if (sOrderType == OrderType.ASC) {
                return comp > 0 ? 1 : -1;
            }
        }
    }
    return 0;
}
```


### 使い方
このCustomCursorJoinerは以下のように使います。

```java
private static final Uri THUMB_URI = MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI;
private static final String[] THUMB_PROJECTION = {MediaStore.Images.Thumbnails.DATA,MediaStore.Images.Thumbnails.IMAGE_ID};
private static final String[] THUMB_WHERE = {String.valueOf(MediaStore.Images.Thumbnails.MINI_KIND)};
private static final String THUMB_ORDER_BY = MediaStore.Images.Thumbnails.IMAGE_ID + " DESC";

private static final Uri IMAGE_MEDIA_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
private static final String IMAGE_ORDER_BY = MediaStore.Images.Media._ID + " DESC";
private static final String[] CONTENT_ORIENTATION = new String[] {
        MediaStore.Images.Media._ID,
        MediaStore.Images.ImageColumns.ORIENTATION
};
private static final String IMAGE_ORDER_BY = MediaStore.Images.Media._ID + " DESC";



String selection = MediaStore.Images.Thumbnails.KIND + " = ? AND "
        + MediaStore.Images.Thumbnails.IMAGE_ID + " IN (" + TextUtils.join(",", imageIdList) + ")";
ContentResolver resolver = context.getContentResolver();
Cursor thumbNailCursor = resolver.query(
        THUMB_URI,
        THUMB_PROJECTION,
        selection,
        THUMB_WHERE,
        THUMB_ORDER_BY);

String rotationSelection = MediaStore.Images.Media._ID + " IN (" + TextUtils.join(",", imageIdList) + ")";
Cursor rotationCursor = resolver.query(
        IMAGE_MEDIA_URI,
        CONTENT_ORIENTATION,
        rotationSelection,
        null,
        IMAGE_ORDER_BY);

CustomCursorJoiner joiner = new CustomCursorJoiner(
        thumbNailCursor,
        new String[]{MediaStore.Images.Thumbnails.IMAGE_ID},
        rotationCursor,
        new String[]{MediaStore.Images.Media._ID},
        CustomCursorJoiner.JoinColumnType.INT,
        CustomCursorJoiner.OrderType.DESC
);
for(CustomCursorJoiner.Result result : joiner){
    if(result == CustomCursorJoiner.Result.BOTH){
        PhotoItem item = new PhotoItem(); // 写真情報(thumbnail, id, 回転情報)を管理するオブジェクト
        item.setThumbNailPath(CursorUtil.getString(thumbNailCursor, MediaStore.Images.Thumbnails.DATA));
        item.setId(CursorUtil.getInt(thumbNailCursor, MediaStore.Images.Thumbnails.IMAGE_ID));
        item.setRotation(CursorUtil.getInt(rotationCursor, MediaStore.Images.ImageColumns.ORIENTATION));
        resultList.add(item);
    }
}
rotationCursor.close();
thumbNailCursor.close();
```

ありがたいことにMediaStore.Images.ImageColumns.ORIENTATIONはdegreeで返ってきます。扱いやすいですね。
あとは以下のようにBitmap生成時にrotation情報を加えてあげて、imageView等にsetしてあげればOKです。

```java
Bitmap bitmap;
bitmap = BitmapFactory.decodeFile(item.getThumbNailPath(), options);

int degree = item.getRotation();
if (degree > 0F) {
    Matrix matrix = new Matrix();
    matrix.postRotate(degree);

    Bitmap rotateBm;
    rotateBm = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);

    bitmap.recycle();
    bitmap = rotateBm;
}
...

```