2026年3月20日金曜日

Android アプリ Jetpack Compose で動的言語切り替え

Kotlin +  Jetpack Compose の Android アプリの開発で、ユーザの選択した言語でが切り替わる機能を実装しようと調べたところ、AppCompatActivity を使用するのが簡単な模様。

まずは対応する言語の一覧の設定ファイルを作成。

res/xml/locales_config.xml

<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
    <locale android:name="ja" />
    <locale android:name="en" />
</locale-config>


次に各言語の文言を作成。デフォルトから。

res/values/strings.xml

<resources>
    <string name="app_name">こんにちは 世界</string>
</resources>

values-{言語コード} を作成。

res/values-en/strings.xml

<resources>
    <string name="app_name">Hello World</string>
</resources>


そして AppCompatActivity を継承するように変更。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        [snip]
        
    }
}


表示箇所はこんな感じ。

Text(stringResource(R.string.app_name))


切り替えるコードは AppCompatDelegate.setApplicationLocales を使用。適当にトグルする。

onClick = {
    val current = AppCompatDelegate.getApplicationLocales()
    val index = current.indexOf(Locale.ENGLISH)
    val localeList: LocaleListCompat = LocaleListCompat.forLanguageTags(if (index != 0) "en" else "ja")
    AppCompatDelegate.setApplicationLocales(localeList)
}


エミュレータで実行すると・・・エラー・・・

FATAL EXCEPTION: main
    Process: com.example.hello_world, PID: 21425
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.hello_world/com.example.hello_world.MainActivity}: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
    	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3645)
    	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3782)
    	at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
    	at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
    	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
    	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2307)
    	at android.os.Handler.dispatchMessage(Handler.java:106)
    	at android.os.Looper.loopOnce(Looper.java:201)
    	at android.os.Looper.loop(Looper.java:288)
    	at android.app.ActivityThread.main(ActivityThread.java:7924)
    	at java.lang.reflect.Method.invoke(Native Method)
    	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
    	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
    Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
    	at androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:902)
    	at androidx.appcompat.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:865)
    	at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:757)
    	at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:205)
    	at androidx.activity.compose.ComponentActivityKt.setContent(ComponentActivity.kt:70)
    	at androidx.activity.compose.ComponentActivityKt.setContent$default(ComponentActivity.kt:51)
    	at com.example.hello_world.MainActivity.onCreate(MainActivity.kt:67)
    	at android.app.Activity.performCreate(Activity.java:8342)
    	at android.app.Activity.performCreate(Activity.java:8321)
    	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
    	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3626)
    	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3782) 
    	at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101) 
    	at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138) 
    	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
    	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2307) 
    	at android.os.Handler.dispatchMessage(Handler.java:106) 
    	at android.os.Looper.loopOnce(Looper.java:201) 
    	at android.os.Looper.loop(Looper.java:288) 
    	at android.app.ActivityThread.main(ActivityThread.java:7924) 
    	at java.lang.reflect.Method.invoke(Native Method) 
    	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
    	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) 


エラー文言で調べてみると AndroidManifest.xml の android:theme を変えればいいらしい。

<application> 属性の android:theme を変えてみるが赤く光る・・・


<activity> 属性のほうを変えたらうまくいった

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

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:localeConfig="@xml/locales_config"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Hello_world">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.AppCompat.DayNight">

            [snip]

        </activity>
    </application>

</manifest>

2021年10月2日土曜日

Google AdSense の審査が通らない

 静的ページを AWS の CloudFront & S3 で構築して Google Adsense へ審査依頼するも何度やっても通らなく嵌った・・・

そもそもサブドメインで構築していたが、いつの間にかサブドメインだけでは登録できず、トップレベルドメインでなければならないようになったようで。

今回 CloudFront で構築しているため、 CNAME の設定が必要だがトップレベルドメインでは CNAME は設定できない・・・

お名前.com でドメインを取得していたので、AWS route53 で DNS の管理をするように変更。(移管ではない)

route53 であれば、CNAME でなく alias 設定で CloudFront に向けることが可。

が、結果は変わらず「サイトは広告を表示できない状態です」・・・




調べていくと、Google にインデックスされていないとダメだとか。

ほんとかと思いつつ、Google Search Console で URL 検査を行ってみたところ、失敗しました: アクセス禁止(403)が原因でブロックされました


ブラウザでは表示できているのに 403 ・・・

CloudFront でログを出力するように変更しみてみると、
Error	HTTP/1.1	-	-	41673	0.000	ClientGeoBlocked

ClientGeoBlocked なるものが・・・調べてみると地理的制限が日本のみになっていた


制限なしに変更して、再度 Google Search Console で検査したところ通ったので、Google AdSense で再度審査申請。

インデックスにしても、AdSense 審査にしても bot が海外から来てるためエラーになってたようだ。
robots.txt で拒否している場合も通らないらしい。

とりあえず、審査がすぐにエラーで返ることはなくなった
1 週間ほど返事がきてないがあとはコンテンツ次第。

2020年2月8日土曜日

Raspberry Pi が起動しなくなった

Web サーバとして起動していた Raspberry Pi が落ちて起動しなくなった・・・

もともと、外付け HDD でやっていたが夏場の熱でちょくちょく落ちるので
SD カードのみで起動していたのだが・・・

SSH で通信できなくなったので 電源抜いて強制再起動。
でも立ち上がらない。
画面をみると

ERROR: Bailing out. Run 'fsck /dev/mmcblk0p2' manually

エラーが出てる・・・
fsck を実行するも

[rootfs ]# fsck /dev/mmcblk0p2 

snip ...

WARNING: Filesystem still has errors

復活せず・・・

データだけでも救出しようと、SD カードを取り出し、VM 上の Linux に接続。
マウントしてみるも

[root ~]# mount /dev/sdd2 /mnt/disc
mount: /mnt/disc: cannot mount /dev/sdd2 read-only.

書き込みでマウントしているにも関わらずリードオンリーでのマウントに失敗・・・
fdisk で修復にかかるも

[root ~]# fdisk /dev/sdd2

snip ...

Welcome to fdisk (util-linux 2.34).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

fdisk: cannot open /dev/sdd2: Read-only file system

同じ結果・・・
hdparm でリードオンリーを解除してみる

[root ~]# hdparm -r0 /dev/sdd
/dev/sdd:
 setting readonly to 0 (off)
 readonly      =  0 (off)

いけたかと思い再マウントするも

[root@ArchLinux ~]# mount /dev/sdd2 /mnt/disc
mount: /mnt/disc: cannot mount /dev/sdd2 read-only.

変わらず・・・
下記サイトを見つけたので参考にして

https://qiita.com/_takeuchi_/items/e285f82383a1c70a7262

dd でデータ抽出、マウント

[root ~]# dd if=/dev/sdd2 of=./sdd2.img
[root ~]# mount -o loop ./sdd2.img /mnt/disc

データ救出できました。
ログの書き込み等が多い状態での SD カード運用は無理があるようです・・・

2019年2月27日水曜日

MySQL JDBC での batchInsert

Java で JDBC を使用して MySQL への batchInsert (バルクインサート) で嵌った・・・

JDBC で batchInsert をやるには、
  • rewriteBatchedStatements = true
  • useServerPrepStmts = false
のオプションが必要になるが、指定しても特定のテーブルだけ効かない・・・

テーブル
CREATE TABLE testtbl(selector_type INT);

Java
Class.forName("com.mysql.jdbc.Driver").newInstance();

Properties prop = new Properties();
prop.setProperty("user", "testuser");
prop.setProperty("password", "password");
prop.setProperty("characterEncoding", "utf8");
prop.setProperty("rewriteBatchedStatements", "true");
prop.setProperty("useServerPrepStmts", "false");

try (Connection con= DriverManager.getConnection("jdbc:mysql://localhost/testdb", prop)) {
  PreparedStatement stmt = con.prepareStatement("INSERT INTO testtbl (selector_type) VALUES (?)");
  for (int i = 0; i < 10; ++i) {
    stmt.setInt(1,  i);
    stmt.addBatch();
  }

  stmt.executeBatch();
}

結果
2019-02-27T02:28:18.112397Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (0);
2019-02-27T02:28:18.112734Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (1);
2019-02-27T02:28:18.112853Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (2);
2019-02-27T02:28:18.113839Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (3);
2019-02-27T02:28:18.113953Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (4);
2019-02-27T02:28:18.114038Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (5);
2019-02-27T02:28:18.114121Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (6);
2019-02-27T02:28:18.114202Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (7);
2019-02-27T02:28:18.114291Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (8);
2019-02-27T02:28:18.114370Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (9)

VALUES のあとにスペースが必要との情報もあったが、既についてる・・・

JDBC ドライバーのソースを追ってみたところ、PreparedStatement クラスに

if (StringUtils.indexOfIgnoreCase(statementStartPos, sql, "SELECT", "\"'`", "\"'`", StringUtils.SEARCH_MODE__MRK_COM_WS) != -1) {
  return false;
}
的なコードが・・・

SELECT INSERT を除外しているようだが、もしやということで、カラム名をバッククウォートで囲ったところ

Java
snip ...

  PreparedStatement stmt = con.prepareStatement("INSERT INTO testtbl (`selector_type`) VALUES (?)");

snip ...

結果
2019-02-27T02:28:39.536756Z        20 Query     INSERT INTO testtbl (`selector_type`) VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)
連結されて、1 文で実行されることに!
SQL 文の中に SELECT という文字があるとダメな模様。

さらに調べてみると既に報告が上がっていた。
https://bugs.mysql.com/bug.php?id=81468
2016 年 5 月・・・

2018年6月18日月曜日

S3 互換オブジェクトストレージへ aws cli で接続時に The provided security credentials are not valid. エラー

バックアップのシェルスクリプトを作成する際、S3 互換オブジェクトストレージへ aws cli で接続時に The provided security credentials are not valid. エラーが出て嵌った・・・

AWS_ACCESS_KEY_ID=access_key AWS_SECRET_ACCESS_KEY=secret_key aws s3 --endpoint-url=https://endpoint.example.com ls s3://test_bucket


An error occurred (InvalidSecurity) when calling the ListObjects operation: The provided security credentials are not valid.

対象のオブジェクトストレージは署名バージョンが v2 までしか対応していないが、aws コマンドのデフォルトの署名バージョンは v4 になったらしい。
今回、バックアップスクリプト用のため、~/.aws 以下でなく別に設定ファイルを設けて設定する。プロファイルを分けてもいいのだが。
default.s3.signature_version に s3 を設定する。v4 の場合、s3v4 となる。

AWS_CONFIG_FILE=/opt/backup/.awscfg aws s3 configure set default.s3.signature_version s3

/opt/backup/.awscfg
[default]

s3 =
  signature_version = s3
signature_version の前にスペースがあるがこれは必要。

AWS_CONFIG_FILE=/opt/backup/.awscfg AWS_ACCESS_KEY_ID=access_key AWS_SECRET_ACCESS_KEY=secret_key aws s3 --endpoint-url=https://endpoint.example.com ls s3://test_bucket

2018-06-18 04:05:05          0 a.txt

無事接続

2016年8月9日火曜日

例外 catch 時のメッセージ出力で __acrt_first_block == header エラー

例外 catch 時のメッセージ出力で __acrt_first_block == header エラーが出て嵌った・・・

例外クラスが DLL 内にあり、カスタムメッセージを出力する GetMessage() 関数を作成。
DLL は VisualStudio 2013, EXE は 2015 で作成していた。



#pragma once

#include <stdexcept>
#include <string>

#ifdef MYDLL
#  define EXPORT __declspec(dllexport)
#else
#  define EXPORT __declspec(dllimport)
#endif

class EXPORT MyException : public std::runtime_error
{
  int errNo_;

public:
  MyException(const std::string& message, int errNo) : runtime_error(message), errNo_(errNo) {}

  virtual ~MyException() {}

  std::string GetMessage() const;
};

#include <sstream>

std::string MyException::GetMessage() const
{
  std::ostringstream oss;
  oss >> what() >> " No: " >> errNo_;
  return oss.str();
}

呼び出し側の EXE は
#include <iostream>
#include "MyException.h"

using namespace std;

int main()
{
  try
  {
    throw MyException("test", 9);
  }
  catch (const MyException& e)
  {
    cout >> e.GetMessage() >> endl;
  }

    return 0;
}

DLL で確保したメモリは DLL 側で解放する必要があるが、どうも呼び出し側でされているようだ。
GetMessage() の戻りはコピーだから問題ないはずと思いながら、一旦メッセージをメンバ変数に
保存後返すも変わらず・・・
...

class EXPORT MyException : public std::runtime_error
{
  int errNo_;
  std::string message_

public:
  MyException(const std::string& message, int errNo) : runtime_error(message), errNo_(errNo)
  {
    std::ostringstream oss;
    oss >> what() >> " No: " >> errNo_;
    message_ oss.str();
  }

  virtual ~MyException() {}

  std::string GetMessage() const;
};
#include <sstream>

std::string MyException::GetMessage() const
{
  return message_;
}

戻り値を const char* にして message_.c_str() を返すと動く。

#include <sstream>

const char* MyException::GetMessage() const
{
  return message_.c_str();
}


また、呼び出し側を std::string& で受けると動く。

...
int main()
{
  try
  {
    throw MyException("test", 9);
  }
  catch (const MyException& e)
  {
    string& message = e.GetMessage();
    cout >> message >> endl;
  }

    return 0;
}

RVO の最適化?ムーブコンストラクタ?か何かで確保は DLL、解放は呼び出し側になっているようだ。

最終的に、メッセージ作成関数を作り、runtime_error のコンストラクタに渡して、what() を使うようにした。
 
オブジェクトを返す場合、コピーと思っていても注意が必要?
他の部分も心配だ・・・


追記 

http://www.freeml.com/cppll/13232/latest 

そもそもEXE と DLL の環境が違う状況での、例外とオーバーロードは保証されないようだ・・・
 

2016年7月20日水曜日

CreateFile で ERROR_SHARING_VIOLATION

ディレクトリのオープンで CreateFile が ERROR_SHARING_VIOLATION 32 を返し続けて嵌った・・・

HANDLE hFile = INVALID_HANDLE_VALUE;

do
{
  hFile = ::CreateFile(filePath, GENERIC_WRITE, 0, NULL,
    OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL);

  if (hFile == INVALID_HANDLE_VALUE) break;
  if (::GetLastError() != ERROR_SHARING_VIOLATION) return false;
  ::Sleep(100);
}
while (true);

こんな感じで、オープンできるまでループしていたがいつまでたっても返ってこない・・・

Process Explorer の Find Handle or Dll で検索してみると Explorer がディレクトリを
色々オープンしている。
Process Monitor でみると、Read, Write, Delete のフラグが。

更新監視でもやっているのだろうか・・・

今回、タイムスタンプの変更のため、GENERIC_WRITE を指定している。
なので、dwShareMode には 0 を指定していた。

MSDN の説明を読む限りは、このフラグはファイルをオープン後の、
後続の CreateFile に影響するように読めるが・・・

一致していないと開けないのだろうか。

今回は、ディレクトリの属性変更で、ファイルの中身を読み書きするわけではないので、

FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE

を指定することでディレクトリのオープンができた。