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
Comments
手元のメモ書きですが、こんな感じになっています。 XamarinComponents/XPlat/ExposureNotification at master · xamarin/XamarinComponents も併せて DL してみてください。
の値が 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;
} |
ボタンを押すと OnClickExposures の処理に入り exposureNotificationService.GetExposureCount() のコードは
userData.ExposureInformation.Count()てのは 関数を呼び出している、のでしょうか?
|
確認なのですが、一致があるのにアプリで接触履歴なしが表示されるのは、 このあたりの 接触あり ContactedNotifyPage var count = exposureNotificationService.GetExposureCount(); で、接触ありだからcountが1以上になるはずなのに、0になっているから、という理解でよいでしょうか。 形式的に、0ではないnilとか入っている可能性もあるのでしょうか。 また、GetExposureCount() がどこで記述されているかなど、調べた方、調べる方いらっしゃいますか。 |
それで
なのですね。ここまでわかりました。 userDataに入っているはずだと。 |
userData public class UserDataService public class UserDataModel 所見 あとは、Androidで動いていてiOSで動いていない対応するコードの差を探してみたりの作業か(私9/13日に講義などあり次の作業はその先になってしまいます。どなたか?) |
@zipperpull さんのコメント
https://twitter.com/zipperpull/status/1303714964398510082 とのことですので上の所見(1だった)を取り下げます。 所見2
|
以下、長いですがコードの解説を流しておきます。 接触者数のコード解析たびたび、iOSが通知として出す「接触数」と、COCOAが「陽性者との接触を確認する」のときに出す接触数が異なるために、混乱が生じています。これを解析するための前段階として、COCOA のホーム画面で「陽性者との接触を確認する」ボタンを押したときの動きの詳細を解説します。 アプリ側で把握する接触数
GetExposureCount() で接触数を取得し、0より大きければ ContactedNotifyPage を表示する 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;
});
}
userData は、UserDataModel 型で、ユーザーのデータを保持しているクラス public class ExposureNotificationService
{
...
public int GetExposureCount()
{
return userData.ExposureInformation.Count();
} ExposureInformation コレクションの保持UserDataModel クラス public class UserDataModel : IEquatable<UserDataModel>
{
...
public ObservableCollection<UserExposureInfo> ExposureInformation { get; set; } = new ObservableCollection<UserExposureInfo>();
} UserExposureInfo クラスは、TEKと同じ値を持つクラスになる 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 メソッド内で行われる。 getExposureInfo がコールバックになっているのは、ExposureInfo の自前で内容を変換できるようにするため、と思われる。ちょっと面倒。ExposureInfo のまま扱ってもいいかもしれない。 [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 関数が呼び出されるのか?実は iOS と Android で呼び出される場所が異なる。 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 が設定される。 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 にあたるものは、 // 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 の呼び出し時に、
が呼び出されている。 iOS では、UpdateKeysFromServer が呼び出されるタイミングは2箇所ある。 FetchExposureKeyAsync 関数内で、UpdateKeysFromServer が呼び出される。 public class ExposureNotificationService
{
public async Task FetchExposureKeyAsync()
{
await Xamarin.ExposureNotifications.ExposureNotification.UpdateKeysFromServer();
}
} FetchExposureKeyAsync 関数は、ホームを開いたときの初期時に呼び出されている。つまり、
ことになる。 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 を更新している部分になる。 ホームのときと同じように、
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 でもバックグラウンドで呼んでいる。 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 関数のなかみは長いので適宜割愛しながら解説する。 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 に渡す必要があるのがややこしいが、
の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 の処理のまとめ
ExposureInformation の追加状態は、バックグラウンドで行われる。
となる。 これにより、userData.ExposureInformation.Count() > 0 のときに「接触者あり」のページに遷移できる。 おそらく、以下の2つの数が異なっているために、接触者あり/なしや通知の時の接触者数の違いが発生していると思われる。
本来ならば、コールバック関数 GetInfo で返す ExposureInfo の数とサマリの MatchedKeyCount が同じになるはずなのだが異なっているのかもしれない(これは推測)。 |
まさにこの現象が発生しているiPhoneが手元にあるので、下記検証してみました。 【検証手順】
【推測】 上記から「接触通知の陽性者判定」と「『陽性者との接触を確認する』ボタンを押した際の陽性者判定」の結果が違うのではないかと推察されます。 ですので、@moonmile さんの仰られている推測に説得力があると考えます。
皆様のご参考になりましたら幸いです |
iOS版で、接触チェックの記録では一致したキーの数が1以上が記録されているのに、
接触確認アプリの「陽性者との接触を確認する」ボタンを押しても「陽性者との接触は確認されませんでした」という画面になる
という不具合に遭遇している事例を多数聞きますが、
これはどこがうまく動いていないのでしょうか?
接触確認アプリの「陽性者との接触を確認する」ボタンを押した後、どのような処理がされているのでしょうか?
1.ボタンを押した時に、アプリが直接接触チェックの記録の中の一致したキーの数を精査して 1以上になっていたら画面に表示
というコードになっているのに、それがうまく動いていない(どこでミスってるの?)のでしょうか?
2. ボタンを押した時、アプリは直接接触チェックの記録の中を見ていない(直接見られない)
アプリはOS側に問い合わせを送りそれの結果を画面に表示している。
2-1. そのOSからの返事が間違っているのでアプリの画面の表示も間違い
2-2. OSからの返事は1件以上とか返事しているのにアプリ側でミスしている
「陽性者との接触を確認する」ボタンを押した後のコードを読まれた人がいましたら詳細を教えてください。
The text was updated successfully, but these errors were encountered: