[Android] [Java] Glide を使って GCS (Google Cloud Storage) 上の画像を Stream でダウンロードする

# Glide
Glide は Android 用の画像読み込み & キャッシュライブラリです．
https://github.com/bumptech/glide

Gradle であれば以下のようにして導入できます．

```build.gradle
repositories {
    mavenCentral()
    google()
}

dependencies {
    implementation 'com.github.bumptech.glide:glide:4.8.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
}
```

# 準備

## 独自モデル
GCS 上に存在するファイルを Bucket と PATH で指定する `GcsImage` というクラスを定義します．

```GcsImage.java
public class GcsImage {

    private final String mBucket;
    private final String mFilename;

    public GcsImage(@NonNull String bucket, @NonNull String filename) {
        mBucket = bucket;
        mFilename = filename;
    }

    public String getBucket() { return mBucket;　}

    public String getFilename() {　return mFilename;　}
}
```

## Glide Module
下記リンクに従って，先ほどの `GcsImage` を読み込むための `ModelLoader` を定義します．
[Writing a custom ModelLoader](https://bumptech.github.io/glide/tut/custom-modelloader.html)
>The first step is to implement the ModelLoader interface. Before we do so, we need to make two decisions:

>1. What type of Model should we handle?
>2. What type of Data should we produce for that Model?

とあるように，`ModelLoader` では，扱う Model と Data 型を指定します．

Model は `String` 等でも良いのですが，今回は独自の `GcsImage` を使用します．
Data 型としては，標準で `InputStream` と `ByteBuffer` のデコーダが用意されています．



```GcsImageLoader.java
public class GcsImageLoader implements ModelLoader<GcsImage, InputStream> {

    private Context mContext;

    public GcsImageLoader(Context context) {
        mContext = context;
    }

    @Nullable
    @Override
    public LoadData<InputStream> buildLoadData(@NonNull GcsImage gcsImage, int width, int height, @NonNull Options options) {
        // バケット名とファイル名が同じであればキャッシュデータを利用するようにします．
        return new LoadData<>(new ObjectKey(gcsImage.getBucket() + '/' + gcsImage.getFilename()), new GcsImageFetcher(gcsImage));
    }

    @Override
    public boolean handles(@NonNull GcsImage gcsImage) {
        return true;
    }

    private class GcsImageFetcher implements DataFetcher<InputStream> {
        private GcsImage gcsImage;

        GcsImageFetcher(GcsImage gcsImage) {
           this.gcsImage = gcsImage;
        }

        @Override
        public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
            try {
                // 実際の読み込み処理を記述します．　ここでは独自のクラスを実装しました．
                InputStream stream = CloudStorageUtils.downloadToStream(mContext, gcsImage.getBucket(), gcsImage.getFilename());
                callback.onDataReady(stream);
            } catch (IOException e) {
                callback.onLoadFailed(e);
            }
        }

        @Override
        public void cleanup() {}

        @Override
        public void cancel() {}

        @NonNull
        @Override
        public Class<InputStream> getDataClass() {
            return InputStream.class;
        }

        @NonNull
        @Override
        public DataSource getDataSource() {
            return DataSource.REMOTE;
        }
    }

    public static class GcsImageLoaderFactory implements ModelLoaderFactory<GcsImage, InputStream> {
        private Context context;

        public GcsImageLoaderFactory(Context context) {
            this.context = context;
        }

        @NonNull
        @Override
        public ModelLoader<GcsImage, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
            return new GcsImageLoader(context);
        }

        @Override
        public void teardown() {}
    }
}
```

`CloudStorageUtils.downloadToStream()` というメソッドも独自に作成したもので，以下のようなクラスに定義しました．

```CloudStorageUtils.java
public class CloudStorageUtils {

    @WorkerThread
    public static InputStream downloadToStream(@NonNull Context context, @NonNull String bucket,
                                               @NonNull String name) throws IOException {
        Storage storage = getStorageService(context);
        Storage.Objects.Get get = storage.objects().get(bucket, name);
        return get.executeMediaAsInputStream();
    }

    @WorkerThread
    @NonNull
    public static Storage getStorageService(@NonNull Context context) throws IOException {
        HttpTransport transport = new NetHttpTransport();
        JsonFactory jsonFactory = new JacksonFactory();
        // 認証情報の取得部分も別クラスで定義しています．
        Credential credential = GoogleApiCredentialFactory.getCredential(context);
        return new Storage(transport, jsonFactory, credential);
    }
}
```

```GoogleApiCredentialFactory.java
public class GoogleApiCredentialFactory {

    private static Credential sCredential;

    public static synchronized Credential getCredential(@NonNull Context context) {
        if (sCredential == null) {
            // JSON ファイルを Stream で読み込み，　Credential を取得します．
            try (InputStream is = context.getAssets().open(BuildConfig.GCP_CREDENTIAL)) {
                List<String> scopes = new ArrayList<>();
                scopes.add(StorageScopes.DEVSTORAGE_FULL_CONTROL);
                GoogleCredential credential = GoogleCredential.fromStream(is);
                sCredential = credential.createScoped(scopes);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return sCredential;
    }
}
```
`BuildConfig.GCP_CREDENTIAL` には，GCP のサービスアカウントキーが記載された JSON ファイルへのパスを指定します．

では，作成した `GcsImageLoader` を追加しましょう．

```GcsImageGlideModule.java
@GlideModule
public class GcsImageGlideModule extends AppGlideModule {

    @Override
    public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
        registry.prepend(GcsImage.class, InputStream.class, new GcsImageLoader.GcsImageLoaderFactory(context));
    }
}
```

`@GlideModule` というアノテーションをつけることにより，Glide は `AppGlideModule` を継承した `GcsImageGlideModule` を認識してくれます．

前述の通り，`build.gradle` には Annotation Processor の記述が必要です．

参照: [Module classes and annotations.](https://bumptech.github.io/glide/doc/configuration.html#module-classes-and-annotations)

# 実装
自作した GlideModule を使って GCS 上の画像を読み込むプログラムは以下の通りです．
ここでは `mImageView` という `ImageView` に画像を表示させています．

```java
Glide.with(this).load(new GcsImage(bucket, filename)).into(mImageView);
```

以上で，無事画像を表示することができました．