Android StudioでのminCompileSdk(31)のビルドエラー

最近、Android Studioでプロジェクト生成後、以下のビルドエラーが表示させる事例が続出している。

The minCompileSdk (31) specified in a dependency's AAR metadata (META-INF/com/android/build/gradle/aar-metadata.properties) is greater than this module's compileSdkVersion (android-30).

この解決法は、Gradle Scriptsを展開して、中のbuild.gradle(Module)を開いて、compileSdktargetSdkの部分を31に書き換える。
右上に[Sync Now]リンクが表示されるので、それをクリックする。すると必要ライブラリがダウンロードされて、ビルドが通るはず。
それにしても、突然、このエラーがなぜ続出するようになったのが、謎。
Google側が強制的に使用しているSDKをアップデートさせたがっているのかな?
と思ってみたり…。

Activity Result APIによる書き換え

Androidアプリで別アクティビティを起動して、そのアクティビティの処理終了後に元のアクティビティで処理を引き継ぐ処理を記述したい場合、これまでは、ActivityクラスにあるstartActivityForResult()メソッドとonActivityResult()メソッドを組み合わせて使っていた。これが、非推奨になった。代わりに、Activity Result APIを利用するようにとのこと。そこで、Androidアプリ開発の教科書の第15章のCameraIntentSampleを題材にして、コードの対応を紹介しておこうと思う。まず、書籍に記載のstartActivityForResult()+onActivityResult()のコードは以下の通りである。

public class MainActivity extends AppCompatActivity {
  :
  public void onCameraImageClick(View view) {
      :
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, _imageUri);
    startActivityForResult(intent, 200);
  }

  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if(requestCode == 200 && resultCode == RESULT_OK) {
      ImageView ivCamera = findViewById(R.id.ivCamera);
      ivCamera.setImageURI(_imageUri);
    }
  }
}

これが、Activity Result APIを使うと、以下のようになる。

public class MainActivity extends AppCompatActivity {
  :
  ActivityResultLauncher<Intent> _cameraLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallbackFromCamera());  // (1)

  public void onCameraImageClick(View view) {
      :
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, _imageUri);
    _cameraLauncher.launch(intent);  // (2)
  }

  private class ActivityResultCallbackFromCamera implements ActivityResultCallback<ActivityResult> {  // (3)
    @Override
    public void onActivityResult(ActivityResult result) {
      if(result.getResultCode() == RESULT_OK) {
        ImageView ivCamera = findViewById(R.id.ivCamera);
        ivCamera.setImageURI(_imageUri);
      }
    }
  }
}

ポイントは、これまでonActivityResult()内に記述していた処理を、ActivityResultCallbackインターフェースをimplementsした専用のコールバッククラスを用意して(3)、onActivityResult()メソッド内に記述し、そのコールバッククラスをもとにActivityResultLauncherを生成するところ(1)。アクティビティの起動は、このActivityResultLauncherのlaunch()メソッドを利用する(2)。
この方式だと、コールバックごとに専用クラスを用意することになり、従来ならrequestCodeによって行っていた分岐が不要になるぶん、確かにコードがスッキリする。なるほど。
Androidアプリ開発の教科書にはKotlin版もあるので、Kotlinでも同様に対比コードを掲載しておこう。

class MainActivity : AppCompatActivity() {
  :
  fun onCameraImageClick(view: View) {
      :
    intent.putExtra(MediaStore.EXTRA_OUTPUT, _imageUri)
    startActivityForResult(intent, 200)
  }

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if(requestCode == 200 && resultCode == RESULT_OK) {
      val ivCamera = findViewById<ImageView>(R.id.ivCamera)
      ivCamera.setImageURI(_imageUri)
    }
  }
}

これが、Activity Result APIを使うと、以下のようになる。

class MainActivity : AppCompatActivity() {
  :
  private val _cameraLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult(), ActivityResultCallbackFromCamera())

  fun onCameraImageClick(view: View) {
      :
    intent.putExtra(MediaStore.EXTRA_OUTPUT, _imageUri)
    _cameraLauncher.launch(intent)
  }

  private inner class ActivityResultCallbackFromCamera : ActivityResultCallback<ActivityResult> {
    override fun onActivityResult(result: ActivityResult?) {
      if(result?.resultCode == RESULT_OK) {
        val ivCamera = findViewById<ImageView>(R.id.ivCamera)
        ivCamera.setImageURI(_imageUri)
      }
    }
  }
}

VirtualBox上のWindowsでのAVD

メインマシンがMac miniに代わって快適に作業ができているのだけど、予想通りひとつだけ困ったのがWindows環境での確認。仕事柄、どうしてもWindowsで確認しないといけないことがある。まあ、古いWindowsマシンをえっちらおっちら起動すればいいのだけど、やはりめんどくさい。そこで、母艦のMac miniVirtualBox上にWindows10をインストールして利用することにしてみた。Boot Campは基本使いたくない。
ほとんどは、この仮想Windows上で問題ないのだが、ひとつ問題なのがAndorid開発でのAVD。AVDは高速化のためにHAXMを利用するが、このHAXMがそもそも仮想化技術を利用したものなので、VirtualBox上のWindowsでは、仮想化の入れ子構造となってしまい、これがサポートされていなかった。
ところが、VirtualBoxの6.1でこのNested Virtualizationがサポートされて、HAXMが利用できるようになった!
実際、今まででは、HAXMのインストールすらできなかったのが、Nested Virtualizationをオンにすると、ちゃんとインストールできた。
また、VirtualCheckerでチェックしてみても、ちゃんとEnabledになっている。以前はUnsupportedだったのに。

ところが、無事、HAXMをインストールできたのに、AVDが起動しない。エラーメッセージは以下の通り。

Emulator: vcpu run failed for vcpu  0

やはり無理か。
ということで、Androidの検証には、しばらく以前の母艦である古いWindowsマシンに頼ることになりそうだ。

PHPでその月の最初の月曜日を取得するときの注意点

PHPで日時処理を行うDateTimeクラスやDateTimeImmutableクラスのmodify()メソッドでは相対的な書式と呼ばれる記述が可能。これが意外と便利。 例えば、3日後の日付を取得しようとした場合、次のような記述が可能。

$now = new DateTimeImmutable();
$threeDaysLater = $now->modify("+3 day");

来月の1日だと次のようになる。

$nextMonth = $now->modify("first day of next month");

で、この方法を利用して、その月の最初の月曜日の日付を取得しようとして次のような記述をすると、思わぬバグに遭遇してしまう。

$aprilfool = new DateTimeImmutable("2020-04-01");
$firstMonday = $aprilfool->modify("first Monday");

このコードだと、 $firstMonday は2020/4/6を表し、確かに2020年4月の最初の月曜日となる。しかし、同じコードを2020年6月に適用すると問題となる。

$junefool = new DateTimeImmutable("2020-06-01");
$firstMonday = $junefool->modify("first Monday");

このコードを実行すると、なんと、 $firstMonday は2020/6/8になってしまう。2020年6月の最初の月曜日は6/1その日なのだ。つまり、 first Monday はその変数の日時から見て「最初の月曜日」という意味で、その月の最初の月曜日ではない。 「その月の最初の月曜日」を表したい場合は、次のように of this month をつける必要がある。

$junefool = new DateTimeImmutable("2020-06-01");
$firstMonday = $junefool->modify("first Monday of this month");

Mac miniと27UK850-Wの接続はType-C

先日、Mac miniが値下げされたのを機に購入することにした。その時にモニタとして選んだのが、LGの27UK850-W。
www.lg.com
Mac miniとの接続方法としてはHDMIを選択したのだが、これだと表示されない。以前使っていたWindowsマシンはHDMI接続で表示されるから、不良ではない。
現象としては、スタートアップのリンゴマークから表示されない。ところが不思議なのは、NVRAMクリアをした場合、2回目のリンゴマークからは表示される。NVRAMクリアでは、2回起動することになるから、2回目の起動はモニタが認識しているといえる。さらに、一度画面が表示された状態で再起動を行った場合も、スタートアップのリンゴマークから表示される。
この現象から考察すると、電源OFFの状態からMac miniを起動した場合、何らかの原因でその起動そのものをモニタが認識できず、結果、何も表示されない状態が続いていてしまうのだろう。
一方、USB Type-Cで接続した場合は、スタートアップのリンゴマークから表示される。
これらの現象をLGのサポートに問い合わせた結果、結局、相性の問題ということに落ち着いた。モニタのファームウェアのアップデートで対応できるのかもしれないが、現段階では対応予定はないみたい。
もし、ボクと同様に、Mac miniと27UK850-Wの組み合わせで利用することを考えている人がいるならば、最初からHDMI接続は諦めて、Type-Cでつなぐようにした方がいい。
ひょっとしたら、27UK850-W以外のLGモニタも、同様に、Mac miniHDMI接続ができないかもしれないので、ご注意を。確認していないが。
ちなみに、モニタそのものの画質には、全く問題なく、満足。

コントローラクラスメソッド内でPhpSpreadsheetを使う際の注意点

Slimのコントローラクラスメソッド内で、PhpSpreadsheetを使ってエクセルファイルを生成し、それをダウンロードさせるコードの場合、注意が必要だ。通常、コントローラクラスメソッドでは、ResponseInterfaceオブジェクトをリターンする必要がある。そのつもりで、次のようなコードを書いたとする。

 :
header("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
header("Content-Disposition: attachment;filename=\"".$filename."\"");
header("Cache-Control: max-age=0");
$writer = IOFactory::createWriter($spreadsheet, "Xlsx");
$writer->save("php://output");
return $response;

これで、ダウンロードされたエクセルファイルを開こうとすると、ファイルが壊れているという警告ダイアログが表示されて、開いたとしても、実際に壊れていることになる。 これを解決するためには、 $writer->save()の次にスクリプトそのものを終了する必要がある。そこで、次のように、 リターンの代わりにexit; を記述する。

 :
header("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
header("Content-Disposition: attachment;filename=\"".$filename."\"");
header("Cache-Control: max-age=0");
$writer = IOFactory::createWriter($spreadsheet, "Xlsx");
$writer->save("php://output");
exit;

Herokuで本番環境のDBをステージング環境のDBにコピー

Herokuのパイプラインを使っていると、DB内の本番環境データをステージング環境のDBにコピーしたい時がある。その場合は、以下のコマンドを使う。
heroku pg:backups:restore sushi::b101 DATABASE_URL --app sushi-staging
この場合、sushiが本番環境のアプリ名、sushi-stagingがステージング環境のアプリ名、b101はコピーしたいバックアップ名を表す。こちらは適宜置き換える。一方、DATABASE_URLは、環境変数を表すので、このまま入力する。
この情報は、Herokuの公式ドキュメントのHeroku PGBackupsのRestoring backupsに載っているが、自分自身の備忘録のためにここに記載してみた。