Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【iOS】接触確認アプリの「陽性者との接触を確認する」ボタンを押した時の処理内容 #24

Open
akizou opened this issue Sep 1, 2020 · 8 comments

Comments

@akizou
Copy link

akizou commented Sep 1, 2020

iOS版で、接触チェックの記録では一致したキーの数が1以上が記録されているのに、
接触確認アプリの「陽性者との接触を確認する」ボタンを押しても「陽性者との接触は確認されませんでした」という画面になる
という不具合に遭遇している事例を多数聞きますが、
これはどこがうまく動いていないのでしょうか?

接触確認アプリの「陽性者との接触を確認する」ボタンを押した後、どのような処理がされているのでしょうか?
1.ボタンを押した時に、アプリが直接接触チェックの記録の中の一致したキーの数を精査して 1以上になっていたら画面に表示
というコードになっているのに、それがうまく動いていない(どこでミスってるの?)のでしょうか?
2. ボタンを押した時、アプリは直接接触チェックの記録の中を見ていない(直接見られない)
アプリはOS側に問い合わせを送りそれの結果を画面に表示している。
2-1. そのOSからの返事が間違っているのでアプリの画面の表示も間違い
2-2. OSからの返事は1件以上とか返事しているのにアプリ側でミスしている

「陽性者との接触を確認する」ボタンを押した後のコードを読まれた人がいましたら詳細を教えてください。

@moonmile
Copy link
Member

moonmile commented Sep 1, 2020

手元のメモ書きですが、こんな感じになっています。

XamarinComponents/XPlat/ExposureNotification at master · xamarin/XamarinComponents

も併せて DL してみてください。

  • userData.ExposureInformation.Count()
  • summary?.MatchedKeyCount

の値が iOS EN api でずれているような気がしています。

陽性者数調査

public Command OnClickExposures => new Command(async () =>
{
    var count = exposureNotificationService.GetExposureCount();
    if (count > 0)
    {
        await NavigationService.NavigateAsync(nameof(ContactedNotifyPage));
        return;
    }
    await NavigationService.NavigateAsync(nameof(NotContactPage));
    return;
});

public int GetExposureCount()
{
    return userData.ExposureInformation.Count();
}

UserDataModel userData;
public ObservableCollection<UserExposureInfo> ExposureInformation { get; set; } = new ObservableCollection<UserExposureInfo>();

ExposureInformation 詰めているところが、

+ ExposureNotificationHandler::ExposureDetectedAsync

var exposureInfo = await getExposureInfo();
// Add these on main thread in case the UI is visible so it can update
await Device.InvokeOnMainThreadAsync(() =>
{
    foreach (var exposure in exposureInfo)
    {
        Debug.WriteLine($"C19R found exposure {exposure.Timestamp}");

        UserExposureInfo userExposureInfo = new UserExposureInfo(exposure.Timestamp, exposure.Duration, exposure.AttenuationValue, exposure.TotalRiskScore, (Covid19Radar.Model.UserRiskLevel)exposure.TransmissionRiskLevel);
        userData.ExposureInformation.Add(userExposureInfo);
    }
});

getExposureInfo 自体はコールバック関数になっているので、

Xamarin.ExposureNotifications
  + ExposureNotification::UpdateKeysFromServer

#if __IOS__
	// On iOS we need to check this ourselves and invoke the handler
	var (summary, info) = await PlatformDetectExposuresAsync(downloadedFiles, cancellationToken);

	// Check that the summary has any matches before notifying the callback
	if (summary?.MatchedKeyCount > 0)
		await Handler.ExposureDetectedAsync(summary, info);
#elif __ANDROID__
	// on Android this will happen in the broadcast receiver
	await PlatformDetectExposuresAsync(downloadedFiles, cancellationToken);
#endif

この PlatformDetectExposuresAsync は、次で定義されている

in ExposureNotification.ios.cs
Xamarin.ExposureNotifications
  + ExposureNotification::PlatformDetectExposuresAsync

この GetInfo が使われる

async Task<IEnumerable<ExposureInfo>> GetInfo()
{
	// Get the info
	IEnumerable<ExposureInfo> info = Array.Empty<ExposureInfo>();
	if (summary?.MatchedKeyCount > 0)
	{
		var exposures = await m.GetExposureInfoAsync(detectionSummary, Handler.UserExplanation, out var exposuresProgress);
		cancellationToken.Register(exposuresProgress.Cancel);
		info = exposures.Select(i =>
		{
			var totalRisk = 0;
			var dictKey = new NSString("totalRiskScoreFullRange");
			if (i.Metadata.ContainsKey(dictKey))
			{
				var sro = i.Metadata.ObjectForKey(dictKey);
				if (sro is NSNumber sron)
					totalRisk = sron.Int32Value;
			}
			else
			{
				totalRisk = i.TotalRiskScore;
			}

			return new ExposureInfo(
				((DateTime)i.Date).ToLocalTime(),
				TimeSpan.FromSeconds(i.Duration),
				i.AttenuationValue,
				totalRisk,
				i.TransmissionRiskLevel.FromNative());
		});
	}
	return info;
}

@akizou
Copy link
Author

akizou commented Sep 1, 2020

ボタンを押すと OnClickExposures の処理に入り
var count = exposureNotificationService.GetExposureCount();
if (count > 0)
ここで 1以上の時と、それ以外に分岐。
後者に行っているということは exposureNotificationService.GetExposureCount() が 0 帰ってきているてことか。

exposureNotificationService.GetExposureCount() のコードは

public int GetExposureCount() {
return userData.ExposureInformation.Count();
これなのかな。userData.ExposureInformation.Count() の結果が入る、と。

userData.ExposureInformation.Count()てのは 関数を呼び出している、のでしょうか?
構造体のメンバを参照しているのでしょうか?

UserDataModel userData;
public ObservableCollection ExposureInformation { get; set; } = new ObservableCollection();
ここから先がわからん。

@kvaluation
Copy link

確認なのですが、一致があるのにアプリで接触履歴なしが表示されるのは、

https://github.com/cocoa-mhlw/cocoa/blob/97e042e444599fe44afbe8046118edd6f8daeab3/Covid19Radar/Covid19Radar/ViewModels/HomePage/HomePageViewModel.cs#L65

このあたりの

接触あり ContactedNotifyPage
接触なし NotContactPage

var count = exposureNotificationService.GetExposureCount();

で、接触ありだからcountが1以上になるはずなのに、0になっているから、という理解でよいでしょうか。

形式的に、0ではないnilとか入っている可能性もあるのでしょうか。

また、GetExposureCount() がどこで記述されているかなど、調べた方、調べる方いらっしゃいますか。

@kvaluation
Copy link

それで

https://github.com/cocoa-mhlw/cocoa/blob/97e042e444599fe44afbe8046118edd6f8daeab3/Covid19Radar/Covid19Radar/Services/ExposureNotificationService.cs#L81

    public int GetExposureCount()
    {
        return userData.ExposureInformation.Count();
    }

なのですね。ここまでわかりました。

userDataに入っているはずだと。

@kvaluation
Copy link

userData

https://github.com/cocoa-mhlw/cocoa/blob/97e042e444599fe44afbe8046118edd6f8daeab3/Covid19Radar/Covid19Radar/Services/ExposureNotificationService.cs#L30

public class UserDataService
/// This service registers, retrieves, stores, and automatically updates user data.
だそうで素晴らしい
https://github.com/cocoa-mhlw/cocoa/blob/97e042e444599fe44afbe8046118edd6f8daeab3/Covid19Radar/Covid19Radar/Services/UserDataService.cs#L34
表示がおかしかった開始日でしょうか。

public class UserDataModel
https://github.com/cocoa-mhlw/cocoa/blob/97e042e444599fe44afbe8046118edd6f8daeab3/Covid19Radar/Covid19Radar/Model/UserDataModel.cs#L90
このへんあやしそう?

所見
・UserDataに matchCount > 1が入っているという証拠は見つからない。
・IsPositived = Trueの UserDataの数 をカウントしている可能性はないでしょうか?

あとは、Androidで動いていてiOSで動いていない対応するコードの差を探してみたりの作業か(私9/13日に講義などあり次の作業はその先になってしまいます。どなたか?)

@kvaluation
Copy link

@zipperpull さんのコメント

UserData は端末ごとにひとつだけなので IsPositive はあまり関係ないです。

ExposureInformation プロパティに値を詰めている処理はまず moonmile さんのコメントを読んでいただければと(MatchedKeyCount も出てきます)

https://twitter.com/zipperpull/status/1303714964398510082

とのことですので上の所見(1だった)を取り下げます。

所見2
return userData.ExposureInformation.Count();

  • userDataにExposureInformationが紐付けられている。
  • ExposureInformationの数を数えているのか、MatchedKeyCountの値を取り出しているかは未確認。

@moonmile
Copy link
Member

moonmile commented Sep 12, 2020

以下、長いですがコードの解説を流しておきます。

接触者数のコード解析

たびたび、iOSが通知として出す「接触数」と、COCOAが「陽性者との接触を確認する」のときに出す接触数が異なるために、混乱が生じています。これを解析するための前段階として、COCOA のホーム画面で「陽性者との接触を確認する」ボタンを押したときの動きの詳細を解説します。

アプリ側で把握する接触数

  1. 「陽性者との接触を確認する」をクリックする.

GetExposureCount() で接触数を取得し、0より大きければ ContactedNotifyPage を表示する

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Covid19Radar/ViewModels/HomePage/HomePageViewModel.cs#L65

public class HomePageViewModel : ViewModelBase {
    ...
    public Command OnClickExposures => new Command(async () =>
    {
        var count = exposureNotificationService.GetExposureCount(); // ※
        if (count > 0)
        {
            await NavigationService.NavigateAsync(nameof(ContactedNotifyPage));
            return;
        }
        await NavigationService.NavigateAsync(nameof(NotContactPage));
        return;
    });
}
  1. userData.ExposureInformation コレクションの数を返す

userData は、UserDataModel 型で、ユーザーのデータを保持しているクラス

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Covid19Radar/Services/ExposureNotificationService.cs#L81

    public class ExposureNotificationService
    {
        ...
        public int GetExposureCount()
        {
            return userData.ExposureInformation.Count();
        }

ExposureInformation コレクションの保持

UserDataModel クラス
https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Covid19Radar/Model/UserDataModel.cs#L98

    public class UserDataModel : IEquatable<UserDataModel>
    {
        ...
        public ObservableCollection<UserExposureInfo> ExposureInformation { get; set; } = new ObservableCollection<UserExposureInfo>();
    }

UserExposureInfo クラスは、TEKと同じ値を持つクラスになる
https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Covid19Radar/Model/UserExposureInfo.cs

    public class UserExposureInfo
    {
        ...
        public DateTime Timestamp { get; }
        public TimeSpan Duration { get; }
        public int AttenuationValue { get; }
        public int TotalRiskScore { get; }
        public UserRiskLevel TransmissionRiskLevel { get; }
    }

この ExposureInformation コレクションが何処かでデータを入れられることにより、アプリ側で「接触数」が1以上になる。

ExposureInformation コレクションへの追加

ExposureInformation コレクションへの追加は、スケジュール(バックグランドタスク)から呼び出される ExposureDetectedAsync メソッド内で行われる。
ExposureDetectedAsync メソッドは、サマリのデータである ExposureDetectionSummary 型と、接触情報の取得をコールバックするための getExposureInfo メソッドが渡される。

getExposureInfo がコールバックになっているのは、ExposureInfo の自前で内容を変換できるようにするため、と思われる。ちょっと面倒。ExposureInfo のまま扱ってもいいかもしれない。

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Covid19Radar/Services/ExposureNotificationHandler.cs#L70

    [Xamarin.Forms.Internals.Preserve] // Ensure this isn't linked out
    public class ExposureNotificationHandler : IExposureNotificationHandler
    {
        // スケジュールから呼び出される
        // this will be called when a potential exposure has been detected
        public async Task ExposureDetectedAsync(ExposureDetectionSummary summary, Func<Task<IEnumerable<ExposureInfo>>> getExposureInfo)
        {
            // サマリを保持しておく
            UserExposureSummary userExposureSummary = new UserExposureSummary(summary.DaysSinceLastExposure, summary.MatchedKeyCount, summary.HighestRiskScore, summary.AttenuationDurations, summary.SummationRiskScore);
            userData.ExposureSummary = userExposureSummary;
            // 接触データの詳細を取得する
            var exposureInfo = await getExposureInfo();

            // UI のために別スレッドにしてある
            // Add these on main thread in case the UI is visible so it can update
            await Device.InvokeOnMainThreadAsync(() =>
            {
                // 取得した exposureInfo のデータを、ExposureInformation に追加していく
                foreach (var exposure in exposureInfo)
                {
                    Debug.WriteLine($"C19R found exposure {exposure.Timestamp}");

                    UserExposureInfo userExposureInfo = new UserExposureInfo(exposure.Timestamp, exposure.Duration, exposure.AttenuationValue, exposure.TotalRiskScore, (Covid19Radar.Model.UserRiskLevel)exposure.TransmissionRiskLevel);
                    // ExposureInformation に追加しているところ
                    userData.ExposureInformation.Add(userExposureInfo);
                }
            });
            // userData を保存する
            await userDataService.SetAsync(userData);

ExposureInformation コレクションに詰め込まれる場所がわかったので、今度は

  • いつ ExposureDetectedAsync が呼び出されるのか?
  • コールバックされる getExposureInfo 関数の内容はどういうものか?

を調べる。

いつ ExposureDetectedAsync 関数が呼び出されるのか?

実は iOS と Android で呼び出される場所が異なる。

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Xamarin.ExposureNotification/ExposureNotification.shared.cs#L84

namespace Xamarin.ExposureNotifications
{
	public static partial class ExposureNotification
    {
		public static async Task<bool> UpdateKeysFromServer(CancellationToken cancellationToken = default)
		{
			var processedAnyFiles = false;

			await Handler?.FetchExposureKeyBatchFilesFromServerAsync(async downloadedFiles =>
			{
				cancellationToken.ThrowIfCancellationRequested();

				if (!downloadedFiles.Any())
					return;

				if (nativeImplementation != null)
				{
					var r = await nativeImplementation.DetectExposuresAsync(downloadedFiles);

					var hasMatches = (r.summary?.MatchedKeyCount ?? 0) > 0;

					if (hasMatches)
						await Handler.ExposureDetectedAsync(r.summary, r.getInfo);  // ※1
				}
				else
				{
#if __IOS__
					// On iOS we need to check this ourselves and invoke the handler
					var (summary, info) = await PlatformDetectExposuresAsync(downloadedFiles, cancellationToken);

					// Check that the summary has any matches before notifying the callback
					if (summary?.MatchedKeyCount > 0)
						await Handler.ExposureDetectedAsync(summary, info);         // ※2
#elif __ANDROID__
					// on Android this will happen in the broadcast receiver
					await PlatformDetectExposuresAsync(downloadedFiles, cancellationToken);
#endif
				}

				processedAnyFiles = true;
			}, cancellationToken);

			return processedAnyFiles;
		}
    }

FetchExposureKeyBatchFilesFromServerAsync でサーバーから zip をダウンロード

※1は、DEBUG_MOCK を指定したときに、呼び出される。テスト環境として nativeImplementation が設定される。
iOS の場合は、※2 で ExposureDetectedAsync が呼び出される
Android 場合は、次の ※3 の箇所になる。

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Xamarin.ExposureNotification/CallbackService.android.cs#L38

namespace Xamarin.ExposureNotifications
{
    [Service(
        Permission = "android.permission.BIND_JOB_SERVICE")]
    [Preserve]
    class ExposureNotificationCallbackService : JobIntentService
    {
        protected override async void OnHandleWork(Intent workIntent)
        {
            Console.WriteLine($"C19R {nameof(ExposureNotificationCallbackService)}");
            var token = workIntent.GetStringExtra(ExposureNotificationClient.ExtraToken);

            var summary = await ExposureNotification.PlatformGetExposureSummaryAsync(token);

            Task<IEnumerable<ExposureInfo>> GetInfo()
            {
                return ExposureNotification.PlatformGetExposureInformationAsync(token);
            }

            // Invoke the custom implementation handler code with the summary info
            Console.WriteLine($"C19R {nameof(ExposureNotificationCallbackService)}{summary?.MatchedKeyCount} Matched Key Count");

            if (summary?.MatchedKeyCount > 0)
            {
                await ExposureNotification.Handler.ExposureDetectedAsync(summary, GetInfo); // ※3
            }
        }

iOS の場合は PlatformDetectExposuresAsync 関数を呼びサマリ情報を取得し、summary?.MatchedKeyCount > 0 をチェックしたのちに、詳細情報を取得するための ExposureDetectedAsync を呼び出す。

Android の場合は PlatformGetExposureSummaryAsync 関数を呼び情報を取得し、summary?.MatchedKeyCount > 0 をチェックしたのちに、詳細情報を取得するために ExposureDetectedAsync を呼び出す。

このとき、コールバック関数の getExposureInfo にあたるものは、
iOS の場合は、PlatformDetectExposuresAsync 関数が返す info

					// On iOS we need to check this ourselves and invoke the handler
					var (summary, info) = await PlatformDetectExposuresAsync(downloadedFiles, cancellationToken);

Android の場合は、PlatformGetExposureInformationAsync の中身になる。

            Task<IEnumerable<ExposureInfo>> GetInfo()
            {
                return ExposureNotification.PlatformGetExposureInformationAsync(token);
            }

iOS の UpdateKeysFromServer 呼び出しタイミング

iOS の場合は、UpdateKeysFromServer の呼び出し時に、

  • サマリを取得する PlatformDetectExposuresAsync
  • 詳細を取得する ExposureDetectedAsync

が呼び出されている。

iOS では、UpdateKeysFromServer が呼び出されるタイミングは2箇所ある。

FetchExposureKeyAsync 関数内で、UpdateKeysFromServer が呼び出される。

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Covid19Radar/Services/ExposureNotificationService.cs#L76

    public class ExposureNotificationService
    {
        public async Task FetchExposureKeyAsync()
        {
            await Xamarin.ExposureNotifications.ExposureNotification.UpdateKeysFromServer();
        }
    }

FetchExposureKeyAsync 関数は、ホームを開いたときの初期時に呼び出されている。つまり、

  1. ホームを開き直すと FetchExposureKeyAsync を呼び出し
  2. 更に UpdateKeysFromServer と呼び出され、
  3. PlatformDetectExposuresAsync でサマリチェックをし、
  4. ExposureDetectedAsync を呼び出すことにより、
  5. 詳細情報の詰め込みが ExposureInformation に行われる、

ことになる。
ただし、HomePageViewModel の Initialize が呼び出されるタイミングは、アプリを起動した時のみ(と思われる)なので、この部分が呼ばれるのはアプリ起動後に1回だけになる。

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Covid19Radar/ViewModels/HomePage/HomePageViewModel.cs#L49

namespace Covid19Radar.ViewModels
{
    public class HomePageViewModel : ViewModelBase
    {
        public override async void Initialize(INavigationParameters parameters)
        {
            // Check Version
            AppUtils.CheckVersion();
            try
            {
                await exposureNotificationService.StartExposureNotification();
                await exposureNotificationService.FetchExposureKeyAsync();
                base.Initialize(parameters);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
        }
    }

もうひとつ iOS ではバックグラウンドでスケジュールが動き ExposureNotification::PlatformScheduleFetch の中で、UpdateKeysFromServer が呼び出されている。こっちがメインで ExposureInformation を更新している部分になる。

ホームのときと同じように、

  1. スケジュールが起動する。
  2. 定期的なスケジュール内で、UpdateKeysFromServer と呼び出され、
  3. PlatformDetectExposuresAsync でサマリチェックをし、
  4. ExposureDetectedAsync を呼び出すことにより、
  5. 詳細情報の詰め込みが ExposureInformation に行われる、
namespace Xamarin.ExposureNotifications
{
	public static partial class ExposureNotification
	{
		static ENManager manager;
        ...
		static Task PlatformScheduleFetch()
		{
			// This is a special ID suffix which iOS treats a certain way
			// we can basically request infinite background tasks
			// and iOS will throttle it sensibly for us.
			var id = AppInfo.PackageName + ".exposure-notification";

			var isUpdating = false;
			BGTaskScheduler.Shared.Register(id, null, task =>
			{
				// Disallow concurrent exposure detection, because if allowed we might try to detect the same diagnosis keys more than once
				if (isUpdating)
				{
					task.SetTaskCompleted(false);
					return;
				}
				isUpdating = true;

				var cancelSrc = new CancellationTokenSource();
				task.ExpirationHandler = cancelSrc.Cancel;

				// Run the actual task on a background thread
				Task.Run(async () =>
				{
					try
					{
						await UpdateKeysFromServer(cancelSrc.Token);    // ※
						task.SetTaskCompleted(true);
					}
					catch (OperationCanceledException)
					{
						Debug.WriteLine($"[Xamarin.ExposureNotifications] Background task took too long to complete.");
					}
					catch (Exception ex)
					{
						Debug.WriteLine($"[Xamarin.ExposureNotifications] There was an error running the background task: {ex}");
						task.SetTaskCompleted(false);
					}

					isUpdating = false;
				});

				scheduleBgTask();
			});

			scheduleBgTask();

			return Task.CompletedTask;
            ...
        }
    }

Android でもバックグラウンドで呼んでいる。

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Xamarin.ExposureNotification/ExposureNotification.android.cs#L238

namespace Xamarin.ExposureNotifications
{
    ...
	public class BackgroundFetchWorker : Worker
	{
		async Task DoAsyncWork()
		{
			if (await ExposureNotification.IsEnabledAsync())
				await ExposureNotification.UpdateKeysFromServer();
		}

iOS でサマリを取得する PlatformDetectExposuresAsync の詳細を調べる

PlatformDetectExposuresAsync の動作を調べていく。

#if __IOS__
					// On iOS we need to check this ourselves and invoke the handler
					var (summary, info) = await PlatformDetectExposuresAsync(downloadedFiles, cancellationToken);

					// Check that the summary has any matches before notifying the callback
					if (summary?.MatchedKeyCount > 0)
						await Handler.ExposureDetectedAsync(summary, info);         // ※2

PlatformDetectExposuresAsync 関数のなかみは長いので適宜割愛しながら解説する。

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Xamarin.ExposureNotification/ExposureNotification.ios.cs#L160

namespace Xamarin.ExposureNotifications
{
	public static partial class ExposureNotification
	{
		static ENManager manager;
        ...
		static async Task<(ExposureDetectionSummary, Func<Task<IEnumerable<ExposureInfo>>>)> PlatformDetectExposuresAsync(IEnumerable<string> keyFiles, CancellationToken cancellationToken)
		{
			// Submit to the API
			var c = await GetConfigurationAsync();
			var m = await GetManagerAsync();

			// Extract all the files from the zips
			var allFiles = new List<string>();
			foreach (var file in keyFiles)
			{
                // key ファイルの読み出し export.bin と export.sig を扱う
                ...
				allFiles.Add(sigTmp); // 詰め込み
			}

            // Apple の サマリ取得の API を呼び出し
            // https://github.com/xamarin/XamarinComponents/blob/master/iOS/ExposureNotification/source/ApiDefinition.cs#L402
            // https://developer.apple.com/documentation/exposurenotification/enmanager/3586331-detectexposureswithconfiguration?language=objc
            // [Export ("detectExposuresWithConfiguration:diagnosisKeyURLs:completionHandler:")]
			// Start the detection
			var detectionSummaryTask = m.DetectExposuresAsync(
				c,
				allFiles.Select(k => new NSUrl(k, false)).ToArray(),
				out var detectProgress);
			cancellationToken.Register(detectProgress.Cancel);
			var detectionSummary = await detectionSummaryTask;

            // 不要ファイルの削除
			// Delete all the extracted files
            ...

			var attDurTs = new List<TimeSpan>();
			var dictKey = new NSString("attenuationDurations");
            // リスク値の取得など
            ..

            // サマリ情報の保持
			var summary = new ExposureDetectionSummary(
				(int)detectionSummary.DaysSinceLastExposure,
				detectionSummary.MatchedKeyCount,
				maxRisk,
				attDurTs.ToArray(),
				sumRisk);

            // 詳細情報を詰め込むコールバック関数
			async Task<IEnumerable<ExposureInfo>> GetInfo()
			{
				// Get the info
				IEnumerable<ExposureInfo> info = Array.Empty<ExposureInfo>();
				if (summary?.MatchedKeyCount > 0)
				{
                    // Apple の EN API の呼び出し
                    // https://github.com/xamarin/XamarinComponents/blob/master/iOS/ExposureNotification/source/ApiDefinition.cs#L407
                    // [Export ("getExposureInfoFromSummary:userExplanation:completionHandler:")]
                    // https://developer.apple.com/documentation/exposurenotification/enmanager/3586332-getexposureinfofromsummary?changes=l_6&language=objc
                    // これは Deprecated になっている。
					var exposures = await m.GetExposureInfoAsync(detectionSummary, Handler.UserExplanation, out var exposuresProgress);
					cancellationToken.Register(exposuresProgress.Cancel);
					info = exposures.Select(i =>
					{
						var totalRisk = 0;
						var dictKey = new NSString("totalRiskScoreFullRange");
						if (i.Metadata.ContainsKey(dictKey))
						{
							var sro = i.Metadata.ObjectForKey(dictKey);
							if (sro is NSNumber sron)
								totalRisk = sron.Int32Value;
						}
						else
						{
							totalRisk = i.TotalRiskScore;
						}

						return new ExposureInfo(
							((DateTime)i.Date).ToLocalTime(),   // UTCのままで十分だが、特に問題なし
							TimeSpan.FromMinutes(i.Duration),
							i.AttenuationValue,
							totalRisk,
							i.TransmissionRiskLevel.FromNative());
					});
				}
				return info;
			}

            // サマリ情報を詳細取得のためのコールバック関数を返す
			// Return everything
			return (summary, GetInfo);  
		}

iOS の場合、ダウンロードした zip を自前で解凍して、Apple の EN API に渡す必要があるのがややこしいが、

  • detectExposuresWithConfiguration:diagnosisKeyURLs:completionHandler:
  • getExposureInfoFromSummary:userExplanation:completionHandler:

の2つの EN API を使っている。

※コールバック処理の GetInfo 内で、totalRisk 値を入れている。実際は、ここで Risk 値の計算などができる気がする。

バックグラウンドのスケジュールの起動タイミング

バックグラウンドの起動設定は、アプリの起動時に行われる。

https://github.com/cocoa-mhlw/cocoa/blob/master/Covid19Radar/Covid19Radar/App.xaml.cs#L57

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace Covid19Radar
{
    public partial class App : PrismApplication
    {
        ...
#if USE_MOCK
            // For debug mode, set the mock api provider to interact
            // with some fake data
            Xamarin.ExposureNotifications.ExposureNotification.OverrideNativeImplementation(new Services.TestNativeImplementation());
#endif
            Xamarin.ExposureNotifications.ExposureNotification.Init();  // ※

            // Local Notification tap event listener
            //NotificationCenter.Current.NotificationTapped += OnNotificationTapped;
            LogUnobservedTaskExceptions();

この PlatformScheduleFetch 内で定期的に UpdateKeysFromServer が呼び出されることになる。

iOS の処理のまとめ

  1. 「陽性者との接触を確認する」をクリックする.
  2. userData.ExposureInformation の数をチェックする。
  3. 0より大きければ、接触者ありのページへ遷移

ExposureInformation の追加状態は、バックグラウンドで行われる。

  1. スケジュールにて、UpdateKeysFromServer が呼び出される。
  2. UpdateKeysFromServer 内で、FetchExposureKeyBatchFilesFromServerAsync で zip のダウンロード
  3. サマリを扱う PlatformDetectExposuresAsync 関数の呼び出し
  4. この時、詳細を詰め込むコールバック関数 GetInfo を返す
  5. サマリで summary?.MatchedKeyCount > 0 であれば、 ExposureDetectedAsync で詳細を取得する
  6. ExposureDetectedAsync 内で、詳細データの ExposureInfo から UserExposureInfo に変換して、ExposureInformation に追加する。

となる。

これにより、userData.ExposureInformation.Count() > 0 のときに「接触者あり」のページに遷移できる。

おそらく、以下の2つの数が異なっているために、接触者あり/なしや通知の時の接触者数の違いが発生していると思われる。

  • summary?.MatchedKeyCount
  • userData.ExposureInformation.Count()

本来ならば、コールバック関数 GetInfo で返す ExposureInfo の数とサマリの MatchedKeyCount が同じになるはずなのだが異なっているのかもしれない(これは推測)。

@ghost ghost added analysis bug Something isn't working 不具合 分析・解析 labels Sep 15, 2020
@igz0
Copy link

igz0 commented Sep 18, 2020

まさにこの現象が発生しているiPhoneが手元にあるので、下記検証してみました。

【検証手順】

  1. 接触確認アプリをアンインストール
  2. 接触確認アプリをApp Storeから再インストール
  3. 接触確認アプリの初期設定を実施
  4. 接触確認アプリから即座に接触通知が表示される
  5. アプリから「陽性者との接触を確認する」ボタンを押下しても陽性者表示は出ず。

【推測】

上記から「接触通知の陽性者判定」と「『陽性者との接触を確認する』ボタンを押した際の陽性者判定」の結果が違うのではないかと推察されます。

ですので、@moonmile さんの仰られている推測に説得力があると考えます。

本来ならば、コールバック関数 GetInfo で返す ExposureInfo の数とサマリの MatchedKeyCount が同じになるはずなのだが異なっているのかもしれない(これは推測)。

皆様のご参考になりましたら幸いです

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants