Skip to content
This repository has been archived by the owner on Apr 12, 2023. It is now read-only.

Conversation

keiji
Copy link
Collaborator

@keiji keiji commented Feb 28, 2021

Ready for review, but contain a commit for debug use only. DO NOT MERGE.

Purpose

Fix issue #25

Does this introduce a breaking change?

[x] Yes
[ ] No

Pull Request Type

What kind of change does this Pull Request introduce?

[x] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Documentation content changes
[ ] Other... Please describe:

How to Test

  1. Launch the app
  2. Reboot device
  3. Watch Logcat
  4. Wait 16 minutes
  5. Confirm background worker succeed on Logcat. (e.g. common_service_locator.log)

#25 において、画面を介さずに起動したバックグラウンドタスクでは、Appに依存したDependencyService.Resolveは使用できないことがわかった。

このPullRequestでは、DependencyService.Resolveに代わりDryIoc.CommonServiceLocatorで依存性の注入(正確にはInversion of Control)を行う経路を新たに追加した

また、ExposureNotificationHandlerの経路上でAppに依存したクラスや処理についても適宜変更を加えている。

今後の課題(iOS対応・テスト)

  • Help Wanted iOSでのテストが必要

@tmurakami
Copy link

現状、本PR で追加したコンテナと Prism が提供する Appクラスのコンテナとの関連がないので、いくつかの Singletonオブジェクトが重複してしまうのではないかと思います。
例えば App クラスにてCreateContainerExtensionをオーバーライドする等して、両者を関連づける必要があるかもしれません。
https://github.com/PrismLibrary/Prism/blob/v7.2.0.1422/Source/Xamarin/Prism.DryIoc.Forms/PrismApplication.cs#L50-L53

子コンテナを作成するのであれば下記が参考になりそうです。
https://github.com/dadhi/DryIoc/blob/4aa249a4ae7499eda28fb97adab088c308771e2b/docs/DryIoc.Docs/KindsOfChildContainer.md

あとDepencencyServiceを使わないのであれば、いくつかのクラス (例えばLogFileServiceAndroid) に付いている[assembly: Dependency(...)]属性は不要になる気がします。
(曖昧な回答で申し訳ありません)

@tmurakami
Copy link

勢いを止めるようで大変心苦しいのですが、本件はキリの良いところで一旦止めておいた方が良いように思います。
開発チーム側でもすでに改修作業に入られているかもしれませんし、作業が被ってしまうのはもったいなく感じます。

@keiji
Copy link
Collaborator Author

keiji commented Feb 28, 2021

おっしゃるとおり、開発チームも改修作業に入っているかもしれませんし、もしかしたら修正済でテストが行われているかもしれません。もしかすると、すでにテストまで完了していて、月曜日にも新しいバージョンがリリースされるかもしれませんね。

以前、こういうコメント( #21 (comment) )をつけたことがあります。

現在のCocoaはオープンソースの方に舵を切りつつあると認識していています。ぼくも言ったことには責任をとらなきゃな。という感じです。

最終的に、Androidで定期実行がうまくいかない問題が解決すればいいわけなので、問題が解決したバージョンがリリースされればもったいなさは許容できる範囲です(使った時間は土、日ですし)。

一方で、開発チーム側で調査が難航していたり、対応策の検討が進んでいない場合、ぼくの作ったコードがなにかの形で開発チーム側の役に立つことも若干ながら期待してたりもします。

あとは、ただのWorkManagerの制約の設定が問題なのだろうと思って開けてみたら、なかなか一筋縄ではいかなくて驚いていたりします。
@tmurakami さんにも、いろいろと指摘をいただけて感謝しています。

@keiji keiji force-pushed the android/fix_targetinvocationexception_occurred_on_worker_task branch from 533f30d to 7467284 Compare February 28, 2021 17:52
Comment on lines 15 to 18
App.InitializeServiceLocator(RegisterPlatformTypes);

App.InitializeExposureNotification();

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOSにおけるAndroidのMainApplicationと同等のタイミングはここ(Main関数)?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppDelegate.FinishedLaunchingだとUI作成が始まってしまう経路という認識なんですよね……(LoadApplication(new App(new iOSInitializer()));してるのもここ)。

iOSだと気にしなくていいのかな。今のiOSでDependencyServiceがちゃんと動いているなら、バックグラウンドタスクでもAppDelegate.FinishedLaunching通ってるって事なんだと思いますが。

.SetRequiredNetworkType(NetworkType.Connected)
.Build());

static TimeSpan bgRepeatInterval = TimeSpan.FromHours(6);
static TimeSpan bgRepeatInterval = TimeSpan.FromMinutes(16);
Copy link
Collaborator Author

@keiji keiji Mar 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

デバッグ(テスト用の変更)。
WorkManager(のバックエンドにあるJobSheduler)はWorkerの実行に際して最短15分の間隔が必要なので、ここは余裕を見て16分に設定している。

@keiji keiji changed the title [WIP]Fix issue that background task fail on Android. Fix issue that background task fail on Android. Mar 1, 2021
@@ -126,11 +127,10 @@ public static void ConfigureBackgroundWorkRequest(TimeSpan repeatInterval, Actio
static Action<PeriodicWorkRequest.Builder> bgRequestBuilder = b =>
b.SetConstraints(new Constraints.Builder()
.SetRequiresBatteryNotLow(true)
.SetRequiresDeviceIdle(true)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

デバッグ(テスト用の変更)。
#27 と同様に、RequiresDeviceIdleを設定すると動作タイミングの予測が難しくなるため制約を削除している。

@tmurakami
Copy link

@keiji 失礼しました。先の発言は、@keiji さん頑張っておられるようなのにマージされなかったらお辛いのではないかと思ってのものでした。

@tmurakami
Copy link

Appクラスについてですが、本PR ではDependencyServiceを使わないことにしているので、以下のコンストラクタではなくbase(initializer)(つまりsetFormsDependencyResolverfalse) を呼び出すほうが良いかもしれません。
https://github.com/cocoa-mhlw/cocoa/blob/d00be84893b413c98a3b5aac1633688e76195181/Covid19Radar/Covid19Radar/App.xaml.cs#L38
COCOA で採用している Prism は 7.2 ですが、8.0 だと今呼び出しているコンストラクタはthis(platformInitializer)となっています。
https://github.com/PrismLibrary/Prism/blob/v8.0.0.1909/src/Forms/Prism.Forms/PrismApplicationBase.cs#L83-L85
また、スーパークラスのPrismApplicationBaseでは、setFormsDependencyResolverを使うコンストラクタは Obsolate になっています(つまりDependencyServiceとの統合はできない)。
https://github.com/PrismLibrary/Prism/blob/v8.0.0.1909/src/Forms/Prism.Forms/PrismApplicationBase.cs#L83-L85

@keiji
Copy link
Collaborator Author

keiji commented Mar 1, 2021

Appクラスについてですが、本PR ではDependencyServiceを使わないことにしているので、以下のコンストラクタではなbase(initializer)(つまりsetFormsDependencyResolverがfalse) を呼び出すほうが良いかもしれません。

このPRでは、Issueの解決を目的としてバックグラウンドタスク周りの処理からDependencyServiceを取り除くことに限定したいなと考えています。なので、関係ない部分のDependencyServiceはそのままにしてあります。

おそらくですが、今回の変更でIssueが解決できることが確認できたら、もう一歩先の取り組みとして「DependencyService自体の全廃」という課題が出てくる気がします。Prism 8.0へのアップグレードは #22 で提案されていたので、それに関連して課題が生まれるものと思われます。

ExposureNotification will be initialized at the entry point of each platform.
Replace IfAlreadyRegistered.Replace with IfAlreadyRegistered.Keep.
@keiji keiji force-pushed the android/fix_targetinvocationexception_occurred_on_worker_task branch from d00be84 to 8fb306b Compare March 1, 2021 15:55
@keiji keiji force-pushed the android/fix_targetinvocationexception_occurred_on_worker_task branch from 8fb306b to 57a1b02 Compare March 2, 2021 15:04
@keiji keiji force-pushed the android/fix_targetinvocationexception_occurred_on_worker_task branch from 57a1b02 to 7b5b158 Compare March 3, 2021 13:29
@keiji keiji force-pushed the android/fix_targetinvocationexception_occurred_on_worker_task branch from 7b5b158 to 238b55c Compare March 4, 2021 10:20
Comment on lines 15 to 17
App.InitializeServiceLocator(RegisterPlatformTypes);

App.UseMockExposureNotificationImplementationIfNeeded();
Copy link
Collaborator Author

@keiji keiji Mar 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

開発担当チームから「iOSの場合、これらの処理は AppDelegateに置く方が適切」とのレビューをもらったので修正する。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

疑うのもあれですが、そのレビューは根拠のある意見なんでしょうか?
どちらに置いても実質的に差異はないとは思うので、普通は AppDelegate にみんな書いているしそれに合わせておいた方がいいかな、くらいの差分しかないような気がします。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうですね。適切というのは自然な書き方としてどうかというニュアンスだと受け取りました。

XCodeでプロジェクト作ってみると、Mainの名の付くファイルはstoryboardでSwiftのようなコードじゃないんですよね。
そういう意味ではiOSわかる人にとってはMainにいろいろ書いてあるのは不自然で、AppDelegateに書いても動作は同じなのだからそちらがより適切。と言うことだと理解しています。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そのニュアンスであればそれ自体には異存はないので移動しても問題ないと思います。

ちなみに、XCode 12.4 でプロジェクトを作ってみましたが main.m が生成されて
UIApplicationMain を呼び出すコードになりました(Objective-C の場合)ので、
普通あまり使われてはいないとは思うものの、過去の経緯としては
全く不自然というわけでもないはずです。

(Swift の場合はこうなるようですが試してはいないです)
https://ez-net.jp/article/BC/vWrNTeBO/85hmtcwh9W3Y/

@keiji keiji force-pushed the android/fix_targetinvocationexception_occurred_on_worker_task branch from e8a53b7 to f138c17 Compare March 20, 2021 14:16
Copy link
Collaborator Author

@keiji keiji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

開発チームの方針を反映しました( f138c17 )。
ExposureNotification.android.csに大きく手を入れているのは、既存のWorkerをキャンセルする必要があるためです。

Comment on lines +16 to +19
var workRequest = CreatePeriodicWorkRequest();
EnqueueUniquePeriodicWork(workRequest);

return Task.CompletedTask;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一度Workerを設定した状態でExistingPeriodicWorkPolicy.KeepでWorkerを設定しても新しい条件(setRequiredDeviceIdle = false)が反映されないため、旧Workerを一度キャンセルして再設定する処理を入れています。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

素朴な疑問としては旧Worker と同じ名前だとダメなんでしょうか?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BackgroundFetchWorker はonStoppedを実装していないので、実行中の Worker は (DB からは削除されるが) 処理が完了するまで止まらないのではないかと思います。
(そもそも今はREPLACEなので、同じ問題はあるのでしょうが...)

Copy link
Collaborator Author

@keiji keiji Mar 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここはアップデートの時に一度だけ実行されるMY_PACKAGE_REPLACEDと違って、何度も実行される部分なので、同じ名前にしてしまうと毎回キャンセルと設定をすることになってReplaceと変わらなくなるからだと理解しています。

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExposureNotificationHandler 側で Interlocked してるから並列で接触チェックが動くことはなさそうですね。
私のコメントは忘れてください。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分自身だと考慮するべきことが増えるのかもしれないですが、Worker 外部からの削除と再登録なので、
名前を変えたから害があるようなことではないですが変える理由としては不思議な感じでした。
ありがとうございます

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@keiji 雑なコメントをしてしまいました。

Worker のキャンセルを ExposureNotificationHandler に伝播させるには

  • onStopped()CancellationTokenSource.Cancel()を呼ぶ
  • (iOS と同じように)UpdateKeysFromServer()に CancellationToken を渡す

必要があると思っているのですが、残念ながらそのようになっていません。

ですので接触チェックが並列実行される可能性があると思っていたのですが、FetchExposureKeyBatchFilesFromServerAsync()Interlocked.Exchange()を使って並列実行しないようにしているので心配ないだろう、ということを言いたかったのでした。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WorkerのonStoppedは、ワーカー停止時の後始末をするために使うメソッド

これはたぶん Activity の onStop みたいなイベントハンドラという意図だと思うのですが
それだと違っているはずで、onStopped は外部から Worker が停止される場合に
そのタイミングで呼ばれて、内部的には Future がキャンセルされるとありますね
(isStopped みたほうがわかりやすい)

https://developer.android.com/reference/androidx/work/ListenableWorker?hl=ja#isStopped()

しかし Worker の doWork の途中でキャンセルされたときその瞬間に勝手に
スレッドが死ぬわけではないと思うので、自分で止まる必要があるんですかね。。(よくわからず)
Xamarin の思想としては CancellationTokenSource は Worker が供給するべきということになりますか?
自分で new して自分で止まるためにつかうこともできますか?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

単純にWorker.doWork()内に処理が書かれているのであればisStopped()を見てキャンセルされているかどうか判断すれば良いと思いますが、ExposureNotificationHandler 内でisStopped()を呼ぶことはできません。
ですので Worker のキャンセルを ExposureNotificationHandler 内で判断できるようにする(例えば下記行で処理を止める)

cancellationToken.ThrowIfCancellationRequested();

ためには、Worker の状態と連動する CancellationToken を Worker 側から ExposureNotificationHandler へ渡す必要があるのではないかと思います。
ExposureNotification.ios.csでは
var cancelSrc = new CancellationTokenSource();
task.ExpirationHandler = cancelSrc.Cancel;

await UpdateKeysFromServer(cancelSrc.Token);

のように BGTask に連動する CancellationToken をUpdateKeysFromServer()に渡しています。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど。Xamarin.EN 側で対称になっていないんですね。。

@@ -95,24 +96,20 @@ public async Task ExposureDetectedAsync(ExposureDetectionSummary summary, Func<T
var exposureInfo = await getExposureInfo();
loggerService.Info($"ExposureInfo: {exposureInfo.Count()}");

// Add these on main thread in case the UI is visible so it can update
await Device.InvokeOnMainThreadAsync(() =>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

元々、このブロックの中でUIに関わる処理をしていたため付けていましたが、現在はMainThreadで実行する必要はなくなったので削除しました。

@keiji
Copy link
Collaborator Author

keiji commented Mar 20, 2021

#62 で、アプリのアップデート時にMY_PACKAGE_REPLACEDのBroadcast Intentを受け取って処理できることがわかっているので、Workerのキャンセルと再設定はそこでするようにすればExposureNotification.android.csの変更は最小限(ExistingPeriodicWorkPolicy.Keepへの変更だけ)で済みそう。

#62 (comment)

@ghost
Copy link

ghost commented Mar 20, 2021

ありやまさんのコードをベースにいけるのであれば当初懸念していたほどの問題はないと思います。

1点だけ、

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Covid19Radar.Android/Services/Logs/LogPeriodicDeleteServiceAndroid.cs#L58-L61

このあたりでオブジェクトを Prism.Ioc とは別に生成する処理は残るのだと思うのですが、
この内部がスレッドセーフではないところがあると思うので、そこが問題ないかを
検証するか修正するかしてもらったほうがいいと思いました
(LoggerService の lockObject がインスタンス変数なので、シングルトンを保証しないと別オブジェクト間では lock していない)

スレッドセーフに関しては InvokeOnMainThreadAsync を消すことで別の問題が起きないかも
充分検証がいるとは思います(iOS 側で検証したことにできるのかもです)

{
return;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserDataのMigrationもここでやってしまっていいかも?

@keiji
Copy link
Collaborator Author

keiji commented Mar 20, 2021

検証手順です。

Visual Studioからのインストールだとアプリの一部しか転送していない(Hot Loading?)らしくMY_PACKAGE_REPLACEDがBroadcastされなかったので、APKファイルを生成する必要があります。

  1. Covid19Radar.Android.csprojectを開いて、EmbedAssembliesIntoApkをtrueに設定する(インストール可能なAPKを生成するように指定)
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

    <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>

  </PropertyGroup>
  1. メニューの[Build] -> [Archive for Publishing]を選択してAPKを生成する
  2. 生成されたCOCOAのAPKファイルをadb install app-debug.apkなどでインストールする。またはGoogle Driveにアップロードして、端末からダウンロード・インストールでも、よりGoogle Playに近い環境でインストールできると思われる
  3. COCOAを起動する(一度起動しないアプリはBroadcast Intentを受け取れない場合があるため)
  4. もう一度、APKファイルをインストールする。
  5. adbやAndroid Studioの[Device File Explorer]を使って/data/data/[COCOAのパッケージ名]/files/log/以下のログを見て、AppVersionMigrateReceiverのログがあることを確認する

ぼくの手元では、5の実行時に次のようなログを確認しました(試行錯誤の過程でバージョンを6に変えていますが5のままでも動きます)。

"2021/03/21 08:13:26","Info","Start","OnReceive","/Volumes/MHLW/repos/cocoa/Covid19Radar/Covid19Radar.Android/AppVersionMigrateReceiver.cs","20","Android","11","Pixel 3","Physical","APP_VERSION","6"
"2021/03/21 08:13:26","Info","Start","CancelWorker","/Volumes/MHLW/repos/cocoa/Covid19Radar/Covid19Radar.Android/AppVersionMigrateReceiver.cs","36","Android","11","Pixel 3","Physical","APP_VERSION","6"
"2021/03/21 08:13:26","Info","End","CancelWorker","/Volumes/MHLW/repos/cocoa/Covid19Radar/Covid19Radar.Android/AppVersionMigrateReceiver.cs","40","Android","11","Pixel 3","Physical","APP_VERSION","6"
"2021/03/21 08:13:26","Info","Start","InitializeExposureNotification","/Volumes/MHLW/repos/cocoa/Covid19Radar/Covid19Radar.Android/AppVersionMigrateReceiver.cs","45","Android","11","Pixel 3","Physical","APP_VERSION","6"
"2021/03/21 08:13:26","Info","End","InitializeExposureNotification","/Volumes/MHLW/repos/cocoa/Covid19Radar/Covid19Radar.Android/AppVersionMigrateReceiver.cs","49","Android","11","Pixel 3","Physical","APP_VERSION","6"
"2021/03/21 08:13:26","Info","End","OnReceive","/Volumes/MHLW/repos/cocoa/Covid19Radar/Covid19Radar.Android/AppVersionMigrateReceiver.cs","31","Android","11","Pixel 3","Physical","APP_VERSION","6"

@keiji keiji force-pushed the android/fix_targetinvocationexception_occurred_on_worker_task branch from 4c3d3a5 to a1f6754 Compare March 21, 2021 00:13
@keiji keiji force-pushed the android/fix_targetinvocationexception_occurred_on_worker_task branch from a1f6754 to f138c17 Compare March 31, 2021 15:13
@keiji
Copy link
Collaborator Author

keiji commented Mar 31, 2021

この変更については一部が開発チームのInternal Repositoryに取り込まれたのでcloseします。

なお、 c81e42a および a1f6754 については今回の取り込み範囲から外れたため、別にPull Requestを出す予定です。

@keiji keiji closed this Mar 31, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants