UnityでVisual Studio 2022を使うときの文字化けを対策する

Unity 2021.3.10f1を使って新しくプロジェクトを作って、C#スクリプトVisual Studio 2022で作成するとUnity側で日本語が文字化けする。原因はVisual Studio文字コードsjisなのに、Unityがutf-8を求めるため。

【目的】新しくプロジェクト作ったとき、最初からutf-8で保存したい
【結論】UnityのUnity Hubのデフォルトの保存場所に.editorconfigを作ってutf-8文字コードを指示する
【詳細・やり方】

    1. Unity Hub 3.3.0を起動して、ギアアイコンをクリック→環境設定→プロジェクト→デフォルトの保存場所を見る
    2. 上記、デフォルトの保存場所に以下の内容で.editorconfigというファイルを作る
    [*]
    end_of_line = crlf
    charset = utf-8
    trim_trailing_whitespace = true
    insert_final_newline = true
    indent_style = space
    indent_size = 4
    3. Unity Hubを再起動する
    4. 既存・新規のUnityプロジェクトでC# scriptをVisual Studioで作成すると最初から utf-8文字コードで作成してくれる

Windows98をWindows10環境で動かす(2022年版)

1.用意するもの

1.1 Windows98 Japanese (MSDN)インストールディスク(CDブート未対応)
- MSDN契約しないともらえない
1.2 Oracle VM VirtulBox バージョン 6.1.38 Windows
https://www.virtualbox.org/wiki/Downloads
1.3 FreeDOS 1.3
http://www.freedos.org/download/
1.4 Win98用のグラフィックドライバ(VirtulBox)140214.zip
https://web.archive.org/web/20190210203844/http://bearwindows.boot-land.net/vbe9x.htm

2.胆になるところ

2.1 セキュリティのメモリ保護にVT-xを使わないようにする(初めからこの設定かも)
  手順: Windows10->スタート→設定→Windows セキュリティ→デバイス セキュリティ→コア分離の詳細→メモリ整合性→オフ
2.2 Win98SEのインストール時 setup.exe /nm /is /ie /c /p j;a とする

3.作業

3.1 Win10に最新のVirtulBoxをインストール(最新じゃないと動かないこと多し)
3.2 FreeDOSサイトからFD13-LiveCD.zipをダウンロードして、FD13LIVE.isoを取り出す
3.3 セキュリティのメモリ保護にVT-xを使わないようにする
  手順: Windows10->スタート→設定→Windows セキュリティ→デバイス セキュリティ→コア分離の詳細→メモリ整合性→オフ
3.4 VirtulBoxマネージャーを起動
3.5 「新規」ボタンを押す
3.6 名前に「Win98SE」といれると
  →良しなにしてくれるので→「作成」を押す
  →良しなにしてくれているので→「作成」を押す
3.7 新しく作ったWin98SEを選択して「設定」ボタンを押す
  ディスプレイ→スクリーン→ビデオメモリを18MBから32MBに変更(後で32MB用のグラフィックドライバを入れるため)
  ストレージ→丸いCDアイコン(空)をクリック
   →右に小さい▼のついた丸いCDアイコンが出るのでクリック
   →ディスクファイルを選択して、あらかじめダウンロードしておいたFD13LIVE.isoを選ぶ
  ネットワーク→ネットワークアダプターを有効化のチェックを外す
  USB→USBコントローラーを有効化のチェックを外す
  「OK」を押してダイアログを閉じる
3.8 「起動」ボタンを押す
(文字いっぱいじゃわからないという場合の公式動画=FreeDOS1.3のインストール)
http://wiki.freedos.org/install/
3.9 Welcome to FreeDOS 1.3 (LiveCD)の画面が出る
「Install to harddisk」を選んでEnter
3.10 Englishを選んでEnter
Yes Continue with the installation→Enter
Yes Partition drive C:→Enter
Yes Please reboot now →Enter
3.11 再びWelcome to FreeDOS 1.3の画面が出る
「Install to harddisk」を選んでEnter
Englishを選んでEnter
Yes Continue with the installation→Enter
Yes Please erase and format drive C:→Enter
US English (Default)→Enter
Plain DOS system→Enter
Yes Please install FreeDOS 1.3→Enter
Yes Please reboot now →Enter
3.12 Boot from system harddiskを選んでEnter
  →1. Load FreeDOS with JEMMEX, no EMS, max RAM freeを選んでEnter
(これでFreeDOSのインストールは終わり。次にWin98のインストール)
3.13 Windows10のCDドライブにWindows98 Japanese (MSDN)ディスクを入れる
3.14 VirualBoxのメニューの「デバイス」「光学ドライブ」「ディスクファイルを選択」で、FD13LIVE.isoが選ばれているが、ホストドライブからCDを入れたドライブに変更 ※変更できない場合は、VirtualBoxマネージャーでWin98SEを選んで
   →「設定」から「ストレージ」→丸いCDアイコン(空)をクリック
   →右に小さい▼のついた丸いCDアイコンが出るのでクリック
   →ホストドライブを選択
3.15 FreeDOSの画面でC:>で、D:と入れる(英語キーボードになっているので注意)
3.16 dir[Enter]すると、JAPANESE.ATとJAPANESE.NECが見えるが、CD JAPANESE.AT
  して、CD WIN98SEする。dir/wするといくつかのCABファイルとSETUP.EXEが見える
3.17 setup.exe /nm /is /ie /c /p j;a ★重要と入力して Enter
3.18 これでWindows98のセットアップが起動する。
3.19 あとは画面の指示に従ってセットアップを進める
3.20 音楽が鳴ってWindows98へようこそダイアログがでます。ただし画面が16色
(これでWindows98のインストールは終わり。
 次にグラフィックドライバのインストール)

3.21 Win98用のグラフィックドライバ(VirtulBox)140214.zipを解凍する。 ※Win98だとクリップボード共有とかホストからのドラッグ&ドロップが使えないため
※Win98だとzip解凍が面倒なので、解凍しておく
3.22 VirualBoxのメニューの「デバイス」「光学ドライブ」「ディスクイメージを選択/作成」で「作成」を選ぶ。左にWin10のファイルが見えるので、140214.zipを解凍した 140214フォルダーを右のWin98SEにドラッグ&ドロップして「OK」を押す。Win98SE.visoが作成されているので、「選択」を押す。
3.23 Win98SEのマイコンピュータをダブルクリックしてDドライブ(CDになっている)から140214/032mbが見えるのを確認する
3.24 Win98SEのデスクトップで画面のプロパティを開いて、「設定」タブの「詳細」ボタンを押す
3.25 「アダプタ」タブの「変更」ボタンを押す→
  「次へ」→特定の場所にあるすべての...を選んで「次へ」
  「ディスクを使用」→「参照」から140214/032mbのフォルダーを選ぶ
  vbemp.infが選ばれる→「OK」
  VBE Miniport - Standard PCI Graphics Adapter (VGA)が選ばれる
  「OK」を押して「次へ」→「完了」→「適用」
3.26 一度Win98を再起動
※Win起動したが真っ黒で何も表示されない場合は、VirtualBox仮想マシンからACPIシャットダウンして、もう一度起動する
3.27 24ビット 1024x768ピクセル が選べるようになる→「適用」

Win10でOracle VM VirtulBoxを使ってWinXpを動かす(2022年版)

1.用意するもの
1.1 アップデート版のXPのインスト―ルCD
1.2 Windows2000のインスト―ルCD
1.3 Oracle VM VirtulBox バージョン 6.1.38 Windows
https://www.virtualbox.org/wiki/Downloads

2.手順
(2.1)アップデート版のXPのインスト―ルCDなので、途中で旧WindowsのCDを入れるように指示されるが、CDを交換しても旧WinCDが認識されない。
そこで、
2.2 Win2000のインストールCDをあらかじめ*.isoイメージにして、HDDに保存。Win10のエクスプローラーで開いて、仮想CDドライブに割り当てておく。
2.3 VirtulBoxをインストールしてWinXP(32bit)を選んで仮想マシンの作成を始める。(容量などの設定はお勧めされるものをそのまま使う)。
2.4 WinXPのCDをCDドライブに入れてWinXPのインストールを開始する。
2.5 途中でWindowsのCD入れ替え指示のときにVirtualBoxから、カレントのCDドライブを変更して、Win2000のCDを認識させて、WinXpのCDドライブにもう一度変更することで、この問題を回避。
VirtualBoxのメニューから「デバイス」「光学ドライブ」から Win10でマウントしているWin2000のISOのドライブを選ぶ。XPを入れてと画面に指示が 出たら同じ操作でWin10のCDドライブに戻す)。
2.6 ネットワークのWinXPアクティベーションはできないので、電話でアクティベーションするを選ぶ(電話ならアクティベーションできる)
2.7 ウイルスが怖いのでWinXPのネットワークはOFFにする。
(VirtualBoxのメニューから「仮想マシン」「設定」「ネットワーク」「アダプター1」で割り当てを「未割り当て」に変更)。
2.8 ホストPC(Win10)で、Windows + IEMicrosoft Update カタログ
 https://www.catalog.update.microsoft.com/Home.aspx
 にアクセスして、『KB936929』で検索すればSP3をダウンロードできる。
 注)検索「SP3」では見つからない。
2.9 VirtualBoxのメニューから「デバイス」「Guest Additions CDイメージの挿入...」  を選択して、出てくるインストール画面に従ってインストール(途中、インストールすると動かなくなるという脅しが出るが無視してインストールを続ける)。
2.10 VirtualBoxのメニューから「デバイス」「クリップボードの共有」と「ドラッグ&ドロップ」の2つを「双方向」に切り替える。
2.11 ホストPC(Win10)で、ダウンロードしていたKB936929-SP3-x86をドラッグ&ドロップでWinXPにコピーして、WinXPで実行してSP3を当てる。再起動後ウイルスの脅威がありますとでるが、何もできないので、WinXPのネットワークは切断したままにしておく。

アセンダンシー(Win95ゲーム)をWindows10で動かす

「アセンダンシー 遥かなる新天地への旅」Windows95/ロジックファクトリーを遊びたい! WindowsXPで起動エラーになるようになって、Win98をバーチャルPCに入れて動かすしかなかったゲーム。

Windows10で直接実行する方法がわかりました。その手順

  1. アセンダンシーのCDをPCに入れて中身を全部WinPCのHDDのどこかにコピーします。
  2. コピーしたフォルダーにある"DATA"フォルダーを"aDATA"に名前を変えます。
  3. コピーしたフォルダーにある"SETUP.EXE"を右クリックして、プロパティを開いて「互換性」タブをクリック、互換モードでこのプログラムを実行する:のチェックボックスをチェックして、Windows 95を選択。「適用」「OK」ボタンを押す。
  4. SETUP.EXEを実行。インストール先をC:Program Files\AscendancyからC:\App\Ascendancyに変更して、「インストール」を押す。インストールが完了したら「中止」ボタンを押す。
  5. コピーしたフォルダーにある"aDATA"フォルダーをC:\App\Ascendancyに移動(C:\App\Ascendancy\aData になる)
  6. C:\App\Ascendancy\wascemd.exeを右クリックして、プロパティを開いて「互換性」タブをクリック、互換モードでこのプログラムを実行する:のチェックボックスをチェックして、Windows 95を選択。「適用」「OK」ボタンを押す。
  7. C:\App\Ascendancy\wascemd.exeを実行する。

情報元: blog.livedoor.jp

Windows10でインターネットに接続できなくなったときの修復方法

Windows10でンターネットに接続できなくなった (1)OS再起動してもダメ (2)ネットワークアダプターの無効→有効してもダメ(削除してもダメ) (3)ネットワーク設定をリセットのダメ (4)DHCPでIPの再取得を試してもダメ (5)違うイーサーアダプター(USB-Ether)を接続してもダメ (6)これで解決:DOS窓から netsh interface ip reset C:\reset.log してOSを再起動

ストレージにある画像をWindowsにアップロードする(Android)続き

1つ前の記事で画像をアップロードできたものの、同じファイルを上書きしてしまう(使えねぇ!!)なので、ファイル名もアップロードするデータに追加して、PHPサーバー側で取り出して、別々のファイルに保存するように改造をします。

まず、Androidアプリから画像を受け取るphpファイル(Windowsにあるよ)を改造します。tomcatをC:\tomcatにインストールしている場合は、 C:\tomcat\htdocs\index.phpというファイルにします。

<?php
$data = file_get_contents("php://input");
$bytes = strlen($data);
$name_part = substr($data, 0, 80);
$name_part_str = "";
foreach (str_split($name_part) as $chr) {
  if ($chr != "\0") {
    $name_part_str .= $chr;
  }
}
$image = substr($data, 80, $bytes - 80);
$fname = "C:/temp/" . $name_part_str . ".jpg";
$fp = fopen($fname, 'wb');
fwrite($fp, $image);
fclose($fp);
echo "Image Upload Finish.\n";
?>

受け取ったデータをsubstr()で先頭の80byteを$name_partに取り出します。$dataはbyte配列なのに、なぜかsubstr()で切れて、結果は文字列になります(ややこしい)。このままファイル名に使えると便利なのですが、null文字が含まれていて、エラーになってしまいます。

str_split()で文字列をbyte配列に戻して(ややこしい)foreachで1つずつ取り出して$chrに保存。null文字を除く文字を$name_part_str .= $chr;でくっつけます。文字結合は+=じゃなくて.=なのです。

画像の本体は、substr($data, 80, $bytes - 80); で80byte目から、最後までを$imageに取り出します。

ファイル名は、$fname = "C:/temp/" . $name_part_str . ".jpg"; で作ります。文字結合は + じゃなくて . なのです。

後は、1つ前の記事と同じで、fopen()でファイルを開いてfwrite()で画像データを保存します。

続いてandroidアプリを改造。前回は送信するデータは画像だけでしたが、データの先頭80byteにファイル名を入れます。ファイル名は、ディレクトリと拡張子を除いた部分 "/storage/emulated/なんたらかんたら/hoge.png"だったら"hoge"のところだけ取り出します。では、PostBmpAsyncHttpRequest.kt の全ソースコード

package com.hatenablog.kuukaix.simpleapp

import android.app.Activity
import android.graphics.Bitmap
import android.os.AsyncTask
import android.view.View
import android.widget.TextView
import java.io.*
import java.net.HttpURLConnection
import java.net.URL

class Param(var uri: String, var bmp: Bitmap, var name: String)

class PostBmpAsyncHttpRequest(private val mActivity: Activity) : AsyncTask<Param?, Void?, String>() {
    override fun doInBackground(vararg params: Param?): String {
        val param: Param = params[0]!!
        var connection: HttpURLConnection? = null
        val sb = StringBuilder()
        try {
            // 画像をjpeg形式でstreamに保存
            val jpg = ByteArrayOutputStream()
            param.bmp.compress(Bitmap.CompressFormat.JPEG, 100, jpg)
            val url = URL(param.uri)
            connection = url.openConnection() as HttpURLConnection
            connection.setConnectTimeout(3000) //接続タイムアウトを設定する。
            connection.setReadTimeout(3000) //レスポンスデータ読み取りタイムアウトを設定する。
            connection.setRequestMethod("POST") //HTTPのメソッドをPOSTに設定する。
            //ヘッダーを設定する
            connection.setRequestProperty("User-Agent", "Android")
            connection.setRequestProperty("Content-Type", "application/octet-stream")
            connection.setDoInput(true) //リクエストのボディ送信を許可する
            connection.setDoOutput(true) //レスポンスのボディ受信を許可する
            connection.setUseCaches(false) //キャッシュを使用しない
            connection.connect()

            // データを投げる
            val out: OutputStream = BufferedOutputStream(connection.getOutputStream())
            val filename = basename(param.name)
            out.write(filename.toByteArray())
            out.write(ByteArray(80 - filename.length))
            out.write(jpg.toByteArray())
            out.flush()

            // データを受け取る
            val istream: InputStream = connection.getInputStream()
            val reader = BufferedReader(InputStreamReader(istream, "UTF-8"))
            var line: String? = ""
            while (reader.readLine().also { line = it } != null) sb.append(line)
            istream.close()
        } catch (e: IOException) {
            e.printStackTrace()
        } finally {
            connection!!.disconnect()
        }
        return sb.toString()
    }

    public override fun onPostExecute(string: String) {
        // 戻り値をViewにセット
        val textView = mActivity.findViewById<View>(R.id.text_view) as TextView
        textView.text = "" + textView.text + "\n" + string
    }

    private fun basename(path: String): String {
        val file = File(path)
        return file.nameWithoutExtension
    }
}

前回から改造したのは

class Param(var uri: String, var bmp: Bitmap, var name: String)

MainActivityからデータを受け取る部分。nameでファイル名を受け取ります。

そしてデータを送信する部分

 // データを投げる
            val out: OutputStream = BufferedOutputStream(connection.getOutputStream())
            val filename = basename(param.name)
            out.write(filename.toByteArray())
            out.write(ByteArray(80 - filename.length))
            out.write(jpg.toByteArray())
            out.flush()
//と
    private fun basename(path: String): String {
        val file = File(path)
        return file.nameWithoutExtension
    }

basename()でファイルを拡張子を除く部分を取り出して、bytearrayにして、out.write()に出力します。 で、これだと80byteにならないので、残りの80 - filename.lengthの長さをnull文字で埋めます。 ByteArray(数)で、数の分だけnull文字が作れます。最後にjpg本体を書き込みます。

今回は、

    public override fun onPostExecute(string: String) {
        // 戻り値をViewにセット
        val textView = mActivity.findViewById<View>(R.id.text_view) as TextView
        textView.text = "" + textView.text + "\n" + string
    }

も追加します。Windowsサーバー側で何らかのエラーが返ると、それをAndroidの画面に出すためです。kotlinでは文字の結合は "."ではなくて"+"なのよね(ややこしい)

で、最後にMainActivityからファイル名をPostBmpAsyncHttpRequest()に渡すよう改造します。

package com.hatenablog.kuukaix.simpleapp

import android.content.ContentUris
import android.graphics.Bitmap
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.button).setOnClickListener {
            // ボタンが押されたら実行される
            Toast.makeText(this, "button clicked", Toast.LENGTH_LONG).show()
            //レコードの取得
            try {
                val contentResolver = contentResolver
                val query = contentResolver.query(
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null
                )
                query?.use { cursor ->
                    val str = String.format(
                            "MediaStore.Images = %s\n\n", cursor.count)
                    val sb = StringBuilder(str)
                    while (cursor.moveToNext()) {
                        sb.append("ID: ")
                        sb.append(cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media._ID)));
                        val id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID))
                        sb.append("\n")
                        sb.append("Title: ")
                        sb.append(cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media.TITLE)))
                        sb.append("\n")
                        sb.append("Path: ")
                        val path_name = cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media.DATA))
                        sb.append(path_name)
                        sb.append("\n")
                        sb.append("Uri: ")
                        val uri = ContentUris.withAppendedId(
                                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                                id)
                        sb.append(uri)
                        sb.append("\n\n")
                        try {
                            val bmp: Bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri)
                            PostBmpAsyncHttpRequest(this).execute(Param("http://192.168.1.12/index.php", bmp, path_name))
                        } catch (e: Exception) {
                            e.printStackTrace()
                            Toast.makeText(this, "なんかエラー", Toast.LENGTH_SHORT).show()
                        }
                    }
                    val textView = findViewById<TextView>(R.id.text_view)
                    textView.setText(sb);
                }
            } catch (e: Exception) {
                e.printStackTrace()
                Toast.makeText(this,
                        "例外が発生、ストレージPermissionを許可していますか?", Toast.LENGTH_SHORT).show()
            }
        }
    }
}
                        val path_name = cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media.DATA))

でファイルパスを取り出して、

                            PostBmpAsyncHttpRequest(this).execute(Param("http://192.168.1.12/index.php", bmp, path_name))

Param()の第3引数にpath_nameを追加して、PostBmpAsyncHttpRequest()に渡します。

これで完成。

Androidアプリを実行してボタンを押すとC:\tempにjpgファイルが作られます。なお、エミュレータでは画像ファイルがないので、画像ファイルの入っている実機androidで試してください。

ストレージにある画像をWindowsにアップロードする(Android)

Androidアプリで画像をアップロードしたいぞぉ!ということで、1つ前の記事でphpをhttpサーバーで動くようにしたので、次は、Androidアプリを作成します。

が、その前にAndroidアプリで画像を受け取るphpファイルをWindowsに作っておきます。tomcatをC:\tomcatにインストールしている場合は、 C:\tomcat\htdocs\index.phpというファイルに

<?php
$data = file_get_contents("php://input");
$fp = fopen("C:/temp/data.jpg", 'wb');
fwrite($fp, $data);
fclose($fp);
echo "Image Upload Finish.\n";
?>

を作ります。でアップロードしたファイルは C:/temp/data.jpg に作るので、C:\tempフォルダーを作っておきます。ここで気が付いたと思いますが、アップロードした画像は全部C:/temp/data.jpgに置かれてしまうので、古い画像は上書きされてしまいます(使えねぇ!!)

2つほど前に記事で作成したボタン付きのSampleAppを改造します。

MainActivityを以下のように改造

"http://192.168.1.21/index.php"のところは、サーバーになるWindows10のPCのローカルアドレスに変更すること。確認するには、Windowsキーを押す→設定→ネットワークとインターネット→プロパティ→IPv4 アドレス

package com.hatenablog.kuukaix.simpleapp

import android.content.ContentUris
import android.graphics.Bitmap
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.button).setOnClickListener {
            // ボタンが押されたら実行される
            Toast.makeText(this, "button clicked", Toast.LENGTH_LONG).show()
            //レコードの取得
            try {
                val contentResolver = contentResolver
                val query = contentResolver.query(
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null
                )
                query?.use { cursor ->
                    val str = String.format(
                            "MediaStore.Images = %s\n\n", cursor.count)
                    val sb = StringBuilder(str)
                    while (cursor.moveToNext()) {
                        sb.append("ID: ")
                        sb.append(cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media._ID)));
                        val id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID))
                        sb.append("\n")
                        sb.append("Title: ")
                        sb.append(cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media.TITLE)))
                        sb.append("\n")
                        sb.append("Path: ")
                        sb.append(cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media.DATA)));
                        sb.append("\n")
                        sb.append("Uri: ")
                        val uri = ContentUris.withAppendedId(
                                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                                id)
                        sb.append(uri)
                        sb.append("\n\n")
                        try {
                            val bmp: Bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri)
                            PostBmpAsyncHttpRequest(this).execute(Param("http://192.168.1.21/index.php", bmp))
                        } catch (e: Exception) {
                            e.printStackTrace()
                            Toast.makeText(this, "なんかエラー", Toast.LENGTH_SHORT).show()
                        }
                    }
                    val textView = findViewById<TextView>(R.id.text_view)
                    textView.setText(sb);
                }
            } catch (e: Exception) {
                e.printStackTrace()
                Toast.makeText(this,
                        "例外が発生、ストレージPermissionを許可していますか?", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

Android Studioでapp→manifests→AndroidManifest.xmlをダブルクリックして開く。 下の方に

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /\>
<uses-permission android:name="android.permission.INTERNET" /\>

の2行を追加。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hatenablog.kuukaix.simpleapp">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SimpleApp">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>

最後にMainAcitivityのところで右クリックしてNew→Kotolin Class/File を選んで、ファイル名にPostBmpAsyncHttpRequestと入力。

中身は

package com.hatenablog.kuukaix.simpleapp

import android.app.Activity
import android.graphics.Bitmap
import android.os.AsyncTask
import java.io.*
import java.net.HttpURLConnection
import java.net.URL

class Param(var uri: String, var bmp: Bitmap)

class PostBmpAsyncHttpRequest(private val mActivity: Activity) : AsyncTask<Param?, Void?, String>() {
    override fun doInBackground(vararg params: Param?): String {
        val param: Param = params[0]!!
        var connection: HttpURLConnection? = null
        val sb = StringBuilder()
        try {
            // 画像をjpeg形式でstreamに保存
            val jpg = ByteArrayOutputStream()
            param.bmp.compress(Bitmap.CompressFormat.JPEG, 100, jpg)
            val url = URL(param.uri)
            connection = url.openConnection() as HttpURLConnection
            connection.setConnectTimeout(3000) //接続タイムアウトを設定する。
            connection.setReadTimeout(3000) //レスポンスデータ読み取りタイムアウトを設定する。
            connection.setRequestMethod("POST") //HTTPのメソッドをPOSTに設定する。
            //ヘッダーを設定する
            connection.setRequestProperty("User-Agent", "Android")
            connection.setRequestProperty("Content-Type", "application/octet-stream")
            connection.setDoInput(true) //リクエストのボディ送信を許可する
            connection.setDoOutput(true) //レスポンスのボディ受信を許可する
            connection.setUseCaches(false) //キャッシュを使用しない
            connection.connect()

            // データを投げる
            val out: OutputStream = BufferedOutputStream(connection.getOutputStream())
            out.write(jpg.toByteArray())
            out.flush()

            // データを受け取る
            val istream: InputStream = connection.getInputStream()
            val reader = BufferedReader(InputStreamReader(istream, "UTF-8"))
            var line: String? = ""
            while (reader.readLine().also { line = it } != null) sb.append(line)
            istream.close()
        } catch (e: IOException) {
            e.printStackTrace()
        } finally {
            connection!!.disconnect()
        }
        return sb.toString()
    }

    public override fun onPostExecute(string: String) {
        // 戻り値をViewにセット
        // val textView = mActivity.findViewById<View>(R.id.text_view) as TextView
        // textView.text = string
    }

}

実行(エミュレータでは画像ファイルがないので正しく動きません。画像が入っている実機で動作させます)すると、画面は

f:id:kuukaix:20210429155156j:plain

で、同じです。「ボタン」を押すと、「例外が発生、ストレージPermissionを許可していますか?」が出ます。 ストレージへアクセス許可しないといけません。Androidの方で「設定」→「アプリ」→「SimpleApp」→「許可」→「ストレージ」のスライドを動かして許可する。permission.INTERNETも追加したのに出ないのは、初めから許可されているからです。(でもAndroidManifest.xmlには書かないといけません)

もう一度アプリを起動して、「ボタン」を押すとMediaStorageに入っている画像データの一覧が出ます。このときhttpのPost機能を使って、画像データをサーバーに転送しています。

Windowsに戻って、C:\temp\data.jpgを見ると最後にアップロードしたファイルが見えます。