Instagramの画像を日替わりでAndroidの壁紙にする

自分で上げたInstagramの画像を日替わりでAndroidの壁紙にランダムに設定します。
また、ついでに、ヤマレコでアップした画像も含むようにしました。

指定した頻度で起動するAndroidアプリと、Instagramまたはヤマレコから画像リストを取得してランダムに選択するNode.jsサーバからなります。

ソースコードもろもろは以下に置いておきました。

https://github.com/poruruba/AndroidWallpaperChanger


# Androidの壁紙の設定

壁紙の設定には、「WallpaperManager」を使います。

(参考)
https://developer.android.com/reference/android/app/WallpaperManager

画像はインターネットから取得します。HTTP Getで取得することを想定しています。
以下のように呼び出すと、InputStreamが取得できます。

```java:Android\Wallpaper_Test\app\src\main\java\jp\or\myhome\sample\wallpaper_test\MainActivity.java
        URL url = new URL(url_str);
        HttpURLConnection con = (HttpURLConnection)url.openConnection();
        InputStream input = con.getInputStream();
```

これをいったん一時的なBitmapに展開します。

```java:Android\Wallpaper_Test\app\src\main\java\jp\or\myhome\sample\wallpaper_test\MainActivity.java
Bitmap image = BitmapFactory.decodeStream(input);
int Iw = image.getWidth();
int Ih = image.getHeight();
```

あとは、以下を呼び出すことで壁紙に設定されるのですが、ちょっと注意が必要です。

```java:Android\Wallpaper_Test\app\src\main\java\jp\or\myhome\sample\wallpaper_test\MainActivity.java
wpm.setBitmap(．．．)
```

壁紙に設定する画像には推奨するサイズがあります。
以下で取得できます。

```java:Android\Wallpaper_Test\app\src\main\java\jp\or\myhome\sample\wallpaper_test\MainActivity.java
        WallpaperManager wpm = WallpaperManager.getInstance(context);
        int Dw = wpm.getDesiredMinimumWidth();
        int Dh = wpm.getDesiredMinimumHeight();
```

推奨サイズの横方向は、スマホの画面の幅よりも4倍ほど大きいです。
これは、ホーム画面が、複数の仮想画面にすることが多いためかと思います。

しかしながら、このサイズに合わない画像を壁紙にしたいことがほとんどかと思いますので、この画像サイズあるいは同じ縦横比のBitmapを用意してあげる必要があります。もしそうしなかった場合、画像の横方向の中心が壁紙の中心にならないです。
ですので、以下の部分で、画像のうち中心に持ってきたい部分を切り抜いて、推奨サイズ(推奨の縦横比の)Bitmapを作成してから、壁紙に設定します。

```java:Android\Wallpaper_Test\app\src\main\java\jp\or\myhome\sample\wallpaper_test\MainActivity.java
        int Ch = (int)((float)Iw / (float)Dw * Dh);
        if( Ih >= Ch ){
            wpm.setBitmap(image, null, true, WallpaperManager.FLAG_SYSTEM);
        }else{
            int Cw = (int)((float)Dw / (float)Dh * Ih);
            int startX = (Iw - Cw) / 2;
            Bitmap bmp = Bitmap.createBitmap(Cw, Ih, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bmp);
            Rect rectSrc = new Rect(startX, 0, startX + Cw, Ih);
            Rect rectDest = new Rect(0, 0, Cw, Ih);
            canvas.drawBitmap(image, rectSrc, rectDest, null);
            wpm.setBitmap(bmp, null, true, WallpaperManager.FLAG_SYSTEM);
        }
```

ちなみに、縦軸方向に縦横比が大きい場合は、仮想画面を左右に切り替えるときに、横方向の移動幅をうまく合わせてくれるようなので、切り抜きはしていません。

ちなみに、WallpaperManager.FLAG_SYSTEMは、ロック画面ではなく、ホーム画面にのみ壁紙を設定するための指定です。

また、以下の権限が必要なので、AndroidManifest.xmlに記載しておきます。

```xml:Android\Wallpaper_Test\app\src\main\AndroidManifest.xml
    <uses-permission android:name="android.permission.SET_WALLPAPER" />
    <uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" />
```

# 定期的に壁紙を変更する

定期的に変更するために、「WorkManager」を使いました。

(参考)
https://developer.android.com/topic/libraries/architecture/workmanager?hl=ja
https://developer.android.com/reference/androidx/work/WorkManager

定期的に起動させたい場合は、「PeriodicWorkRequest」を使います。

```java:Android\Wallpaper_Test\app\src\main\java\jp\or\myhome\sample\wallpaper_test\MainActivity.java
                PeriodicWorkRequest request;
                switch(interval){
                    case 1: {
                        // 一時間ごと
                        request = new PeriodicWorkRequest.Builder( WallpaperChangeWorker.class, 1, TimeUnit.HOURS)
                                .addTag(WallpaperChangeWorker.WORKER_TAG)
                                .build();
                        break;
                    }
                    case 2: {
                        // 15分ごと
                        request = new PeriodicWorkRequest.Builder( WallpaperChangeWorker.class, 15, TimeUnit.MINUTES)
                            .addTag(WallpaperChangeWorker.WORKER_TAG)
                            .build();
                        break;
                    }
                    default: {
                        // 一日ごと
                        request = new PeriodicWorkRequest.Builder( WallpaperChangeWorker.class, 1, TimeUnit.DAYS)
                                .addTag(WallpaperChangeWorker.WORKER_TAG)
                                .build();
                        break;
                    }
                }
```

今回は、一日ごと、一時間ごと、15分ごとを選べるようにしました。
これにより、定期的に、引数に指定したWallpaperChangeWorkerクラスのdoWorkメソッドが呼ばれます。そのクラスは、Workerを継承している必要があります。

あとは、以下を呼び出すだけです。

```java:Android\Wallpaper_Test\app\src\main\java\jp\or\myhome\sample\wallpaper_test\MainActivity.java
manager.enqueueUniquePeriodicWork(WallpaperChangeWorker.WORKER_TAG, ExistingPeriodicWorkPolicy.REPLACE, request);
```

ExistingPeriodicWorkPolicy.REPLACE は、すでにWorkRequestで起動させていた場合にそれを置き換えるために指定します。そうしないと、呼び出すたびにWorkerが多重に起動されてしまうためです。

インターネット接続しているため、AndroidManifest.xmlに以下を追記します。

```xml:Android\Wallpaper_Test\app\src\main\AndroidManifest.xml
    <uses-permission android:name="android.permission.INTERNET" />
```

# ソースコード一式(Android)

すべてを一つにまとめるとこうなります。

```java:Android\Wallpaper_Test\app\src\main\java\jp\or\myhome\sample\wallpaper_test\MainActivity.java
package jp.or.myhome.sample.wallpaper_test;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import androidx.work.PeriodicWorkRequest;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.WindowMetrics;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    public static final String TAG = "LogTag";
    WorkManager manager;
    static int Sw;
    static int Sh;
    static String media_url = "https://【立ち上げたNode.jsサーバのホスト名】/wallpaper-get";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        manager = WorkManager.getInstance(this);

        WindowMetrics windowMetrics = this.getWindowManager().getCurrentWindowMetrics();
        Sw = windowMetrics.getBounds().width();
        Sh = windowMetrics.getBounds().height();
        Log.d( TAG, "ScreenWidth=" + Sw + " ScreenHeight=" + Sh);

        ArrayAdapter<String> adapter = new ArrayAdapter<>(
                this,
                android.R.layout.simple_spinner_item,
                new String[]{ "一日ごと", "一時間ごと", "15分ごと" }
        );
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        Spinner spin;
        spin = (Spinner)findViewById(R.id.spin_interval);
        spin.setAdapter(adapter);

        Button btn;
        btn = (Button)findViewById(R.id.btn_update_wallpaper);
        btn.setOnClickListener(this);
        btn = (Button)findViewById(R.id.btn_stop_wallpaper);
        btn.setOnClickListener(this);
        btn = (Button)findViewById(R.id.btn_update_status);
        btn.setOnClickListener(this);

        updateStatus();
    }

    private void updateStatus(){
        TextView text;
        text = (TextView)findViewById(R.id.txt_status);
        try {
            ListenableFuture<List<WorkInfo>> listenable = manager.getWorkInfosForUniqueWork(WallpaperChangeWorker.WORKER_TAG);
            List<WorkInfo> list = listenable.get();
            int num = list.size();
            if( num == 0 ){
                text.setText("Not Running");
            }else
            if(num > 1 ) {
                throw new Exception("error list.size() != 1");
            }else{
                text.setText(list.get(0).getState().toString());
            }
        }catch(Exception ex){
            Toast.makeText(this, ex.getMessage(), Toast.LENGTH_LONG).show();
            text.setText("Error");
        }
    }

    private static void updateWallpaper(Context context, String url_str) throws Exception{
        WallpaperManager wpm = WallpaperManager.getInstance(context);
        int Dw = wpm.getDesiredMinimumWidth();
        int Dh = wpm.getDesiredMinimumHeight();
        Log.d( TAG, "desiredMinimumWidth=" + Dw + " desiredMinimumHeight=" + Dh);

        URL url = new URL(url_str);
        HttpURLConnection con = (HttpURLConnection)url.openConnection();
        InputStream input = con.getInputStream();

        Bitmap image = BitmapFactory.decodeStream(input);
        int Iw = image.getWidth();
        int Ih = image.getHeight();
        Log.d( TAG, "imageWidth=" + Iw + " imageHeight=" + Ih);

        int Ch = (int)((float)Iw / (float)Dw * Dh);
        if( Ih >= Ch ){
            wpm.setBitmap(image, null, true, WallpaperManager.FLAG_SYSTEM);
        }else{
            int Cw = (int)((float)Dw / (float)Dh * Ih);
            int startX = (Iw - Cw) / 2;
            Bitmap bmp = Bitmap.createBitmap(Cw, Ih, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bmp);
            Rect rectSrc = new Rect(startX, 0, startX + Cw, Ih);
            Rect rectDest = new Rect(0, 0, Cw, Ih);
            canvas.drawBitmap(image, rectSrc, rectDest, null);
            wpm.setBitmap(bmp, null, true, WallpaperManager.FLAG_SYSTEM);
        }

        Log.d(TAG, "Wallpaper Updated");
    }

    @Override
    public void onClick(View view) {
        switch(view.getId()){
            case R.id.btn_update_wallpaper:{
                Spinner spin;
                spin = (Spinner) findViewById(R.id.spin_interval);
                int interval = spin.getSelectedItemPosition();

                PeriodicWorkRequest request;
                switch(interval){
                    case 1: {
                        // 一時間ごと
                        request = new PeriodicWorkRequest.Builder( WallpaperChangeWorker.class,1, TimeUnit.HOURS)
                                .addTag(WallpaperChangeWorker.WORKER_TAG)
                                .build();
                        break;
                    }
                    case 2: {
                        // 15分ごと
                        request = new PeriodicWorkRequest.Builder( WallpaperChangeWorker.class, 15, TimeUnit.MINUTES)
                            .addTag(WallpaperChangeWorker.WORKER_TAG)
                            .build();
                        break;
                    }
                    default: {
                        // 一日ごと
                        request = new PeriodicWorkRequest.Builder( WallpaperChangeWorker.class,1, TimeUnit.DAYS)
                                .addTag(WallpaperChangeWorker.WORKER_TAG)
                                .build();
                        break;
                    }
                }

                manager.enqueueUniquePeriodicWork(WallpaperChangeWorker.WORKER_TAG, ExistingPeriodicWorkPolicy.REPLACE, request);

                Toast.makeText(this, "壁紙の更新を要求しました。", Toast.LENGTH_LONG).show();
                updateStatus();
                break;
            }
            case R.id.btn_stop_wallpaper:{
                manager.cancelUniqueWork(WallpaperChangeWorker.WORKER_TAG);
                updateStatus();
                break;
            }
            case R.id.btn_update_status:{
                updateStatus();
                break;
            }
        }
    }

    public static class WallpaperChangeWorker extends Worker {
        Context context;
        final public static String WORKER_TAG = "WallpaperChangeWorkerTAG";

        public WallpaperChangeWorker(Context context, WorkerParameters workerParams) {
            super(context, workerParams);

            this.context = context;
        }

        public Result doWork() {
            try{
                updateWallpaper(context, media_url);
            }catch(Exception ex){
                Log.d(TAG, ex.getMessage());
                return Result.retry();
            }

            return Result.success();
        }
    }
}
```

# Instagramの画像リストを取得する

以下を参照してください。

[Instagramにアップロードした画像をランダムにESP32に表示する](https://qiita.com/poruruba/items/c25caeb77bbd1074c986)

手抜きですみません。
アクセストークンは、60日期限のため、1か月に1回cronにより更新するようにしています。

# ヤマレコの画像リストを取得する

ヤマレコの画像リストは、スライドショーを表示するときのページからスクレーピングしました。
スライドショーは、以下のURLです。recIdは山行記録のIDです。

```https://www.yamareco.com/modules/yamareco/include/slideshow.php?did=[recId]```

あとは、cheerioでパースします。

```js:nodejs\api\controllers\wallpaper-get\index.js
async function get_all_yamareco_image_list(recId){
	var url = yamareco_slideshow_url + recId;
	var response = await do_get_text(url);

	var $ = cheerio.load(response);
	var list = [];
	$(".hidden-container > a").each(function() {
		var link = $(this);
		var href = link.attr("href");
		list.push({ media_url: href });
	});

	return list;
}
```

ユーザIdからrecIdの取得については、/getReclistを使っていますが、詳しくは以下を参照してください。

[ヤマレコの軌跡をLeafletで表示する](https://qiita.com/poruruba/items/88e23011815e8e0f4ffb)


取得する対象ユーザのIDは、 ```nodejs\data\wallpaper\userid_list.json``` に記載しておきます。

# ランダム画像を呼び出し側に返す

HTTP Getでアクセスしてきたら、画像を返します。
返すといっても、画像があるURLにリダイレクトするように返すだけです。
以下に示す通り、一通り候補となる画像のURLのリストを集めたのち、ランダムに選んで、そのURLをリダイレクト指定で返しています。

```js:nodejs\api\controllers\wallpaper-get\index.js
		var index = make_random(list.length - 1);
		
		return new Redirect(list[index].media_url);
```

# ソースコード一式(Node.js)

ソースコードをまとめるとこんな感じです。

```js:nodejs\api\controllers\wallpaper-get\index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || "/opt/";
const Response = require(HELPER_BASE + 'response');
const Redirect = require(HELPER_BASE + 'redirect');

const cheerio = require('cheerio');

const jsonfile = require(HELPER_BASE + 'jsonfile-utils');

const yamareco_base_url = "https://api.yamareco.com/api/v1";
const yamareco_slideshow_url = "https://www.yamareco.com/modules/yamareco/include/slideshow.php?did=";
const instagram_image_list_url = 'https://graph.instagram.com/me/media';
const instagram_refresh_url = 'https://graph.instagram.com/refresh_access_token';

const fetch = require('node-fetch');

const INITIAL_ACCESS_TOKEN = '【Instagramの初期トークン】';

const TOKEN_FILE_PATH = process.env.THIS_BASE_PATH + '/data/wallpaper/access_token.json';
const USERIDLIST_FILE_PATH = process.env.THIS_BASE_PATH + '/data/wallpaper/userid_list.json';

exports.handler = async (event, context, callback) => {
	if( event.path == '/wallpaper-get' ){
		console.log(event.queryStringParameters);

		var userid_list = await jsonfile.read_json(USERIDLIST_FILE_PATH, [] );
		console.log(userid_list);

		var list = [];
		for( let item of userid_list ){
			var list_yamareco = await get_all_yamareco_list(item);
//			console.log(list_yamareco);
			list = list.concat(list_yamareco);
		}

		var json = await read_token();
		var list_instagram = await get_all_image_list(json.access_token);
//		console.log(list_instagram);

		var list = list.concat(list_instagram);

		var index = make_random(list.length - 1);
		
		return new Redirect(list[index].media_url);
	}else{
		throw "unknown endpoint";
	}
};

async function get_all_yamareco_list(userId){
	var counter = 1;
	var response = await do_get(yamareco_base_url + "/getReclist/user/" + userId + '/' + counter++);
//	console.log(response);
	if( response.err != 0 )
		throw response.errcode;
	var reclist = response.reclist;
	var list = [];
	for( let item of reclist ){
		var response_list = await get_all_yamareco_image_list(item.rec_id);
		list = list.concat(response_list);
	}

	return list;
}

async function get_all_yamareco_image_list(recId){
	var url = yamareco_slideshow_url + recId;
	var response = await do_get_text(url);

	var $ = cheerio.load(response);
	var list = [];
	$(".hidden-container > a").each(function() {
		var link = $(this);
		var href = link.attr("href");
		list.push({ media_url: href });
	});

	return list;
}

async function get_all_image_list(access_token){
    console.log("get_all_image_list called");
    var list = [];
    var url = instagram_image_list_url + '?fields=id,caption,permalink,media_url&access_token=' + access_token;
    do{
        var json = await do_get(url);
        list = list.concat(json.data);
        if( !json.paging.next )
            break;
        url = json.paging.next;
    }while(true);

    return list;
}

function do_get_text(url, qs) {
  var params = new URLSearchParams(qs);

  var params_str = params.toString();
  var postfix = (params_str == "") ? "" : ((url.indexOf('?') >= 0) ? ('&' + params_str) : ('?' + params_str));
  return fetch(url + postfix, {
      method: 'GET',
    })
    .then((response) => {
      if (!response.ok)
        throw new Error('status is not 200');
      return response.text();
    });
}

function do_get(url, qs) {
  var params = new URLSearchParams(qs);

  var params_str = params.toString();
  var postfix = (params_str == "") ? "" : ((url.indexOf('?') >= 0) ? ('&' + params_str) : ('?' + params_str));
  return fetch(url + postfix, {
      method: 'GET',
    })
    .then((response) => {
      if (!response.ok)
        throw new Error('status is not 200');
      return response.json();
    });
}

async function read_token(){
	var json = await jsonfile.read_json(TOKEN_FILE_PATH, { access_token: INITIAL_ACCESS_TOKEN } );
	if( !json.expires_in ){
		json = await refresh_token();
	}
	return json;
}

async function refresh_token(){
	var json = await jsonfile.read_json(TOKEN_FILE_PATH, { access_token: INITIAL_ACCESS_TOKEN } );

	var url = instagram_refresh_url + '?grant_type=ig_refresh_token&access_token=' + json.access_token;
	var result = await do_get(url);
	json.access_token = result.access_token;
	json.expires_in = result.expires_in;

	await jsonfile.write_json(TOKEN_FILE_PATH, json);
	return json;
}

exports.trigger = async (event, context, callback) => {
	console.log('wallpaper cron triggered');

	await refresh_token();
};

function make_random(max) {
	return Math.floor(Math.random() * (max + 1));
}
```

以上