質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.00%

【Xamarin.Forms】バックグラウンドで常に位置情報を送り続けたい【Android】

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,948

VEX

score 88

開発環境

Visual Studio 2017 Community 15.9.4
Xamarin.Forms 3.4.0.1009999
Xamarin.Forms.GoogleMaps 3.0.4
Xam.Plugin.Geolocator 4.5.0.6

やりたいこと

Xamarin.Formsを用い、Androidアプリを開発しています。
常に位置情報を送りたいので、サービスを実装したいのですが上手く動いてくれません。
AndroidでOS起動時に自動実行するバックグラウンドサービスの作成方法
バックグラウンドサービスからDependencyServiceをコールする方法
上記リンクを参考に実装したのですが、情報が古いのでしょうか?
AndroidのバージョンはLolipop以降を想定しています。
今後iOSにも対応する予定の為、一部DependencyService等を使用しています。
上記リンク先にあったサンプルコードを実行してみましたが動作しませんでした。

コード

実際に実装したコードは以下になります。

BackgroundService.cs
namespace App.Droid
{
    [Service(Name = "com.CompanyName.App.BackgroundService", Exported =true, Process =":Process")]
    public class BackgroundService : Service
    {
        public override IBinder OnBind(Intent intent)
        {
            return null;
        }

        [return: GeneratedEnum]
        public override StartCommandResult OnStartCommand(Intent intent, [GeneratedEnum] StartCommandFlags flags, int startId)
        {
            // Oreo対応
            if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
            {
                this.RegisterForegroundService();
            }

            Thread t = new Thread(() =>
            {
                var bundle = new Bundle();
                global::Xamarin.Forms.Forms.Init(this, bundle);

                App.BackgroundThread.Main();
            });
            t.Start();

            return StartCommandResult.Sticky;
        }

        void RegisterForegroundService()
        {
            var notification = new Notification.Builder(this)
                                    .SetContentTitle("アプリ")
                                    .SetContentText("Start ForegroundService")
                                    .SetSmallIcon(Resource.Mipmap.icon) // Android7.0対応
                                    .SetColor(ActivityCompat.GetColor(Android.App.Application.Context, Resource.Color.notification_color)) // Android7.0対応
                                    .SetOngoing(true)
                                    .Build();

            this.StartForeground(99999, notification);
        }

        public void StartBackgroundService()
        {
            Intent serviceIntent = new Intent(this, typeof(BackgroundService));

            // Lollipop対応
            if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop &&
                Build.VERSION.SdkInt <= BuildVersionCodes.LollipopMr1)
            {
                string packageName = this.PackageManager.GetPackageInfo(this.PackageName, 0).PackageName;
                serviceIntent.SetPackage(packageName);
            }
            else
            {
                serviceIntent.AddFlags(ActivityFlags.NewTask);
            }

            base.StartService(serviceIntent);
        }

        public override void OnDestroy()
        {
            base.OnDestroy();
            this.StartBackgroundService();
        }
    }
}
BootReceiver.cs
namespace App.Droid
{
    [BroadcastReceiver]
    [IntentFilter(new[] { Intent.ActionBootCompleted,
                           "android.intent.action.QUICKBOOT_POWERON",
                           "com.htc.intent.action.QUICKBOOT_POWERON",
                           "android.intent.action.PACKAGE_INSTALL",
                           "android.intent.action.PACKAGE_ADDED",
                           Intent.ActionMyPackageReplaced
    })]
    public class BootReceiver : BroadcastReceiver
    {
        public BootReceiver() : base()
        {
        }

        public override void OnReceive(Context context, Intent intent)
        {
            Intent serviceIntent = new Intent(context, typeof(BackgroundService));

            // Lollipop対応
            if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop &&
                Build.VERSION.SdkInt <= BuildVersionCodes.LollipopMr1)
            {
                string packageName = context.PackageManager.GetPackageInfo(context.PackageName, 0).PackageName;
                serviceIntent.SetPackage(packageName);
                serviceIntent.SetClassName(context, packageName + ".BackgroundService");
            }
            else
            {
                serviceIntent.AddFlags(ActivityFlags.NewTask);
            }

            // Oreo対応
            if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
            {
                context.StartForegroundService(serviceIntent);
            }
            else
            {
                context.StartService(serviceIntent);
            }
        }
    }
}
BackgroundThread.cs
namespace App
{
    public static class BackgroundThread
    {
        public static async void Main()
        {
            DependencyService.Get<INotificationService>().Regist();
            DependencyService.Get<ILocationService>().Initialize();
            DependencyService.Get<ILocationService>().OnLocationChanged += new OnLocationChangedDelegate(GlobalClass.Instance.LocationChanged);
            DependencyService.Get<ILocationService>().OnLocationError += new OnLocationErrorDelegate(GlobalClass.Instance.LocationError);
            DependencyService.Get<ILocationService>().StartListening(10000, 10, true);

            while (true)
            {
                Plugin.Geolocator.Abstractions.Position pos = await DependencyService.Get<ILocationService>().GetPositionAsync(1000);

                // 送信処理
                Communication.AddParam("Command", "pos");
                Communication.AddParam("Lat", pos.Latitude.ToString());
                Communication.AddParam("Lng", pos.Longitude.ToString());
                string response = await Communication.Send();

                DependencyService.Get<INotificationService>().On("通信結果", response);

                await Task.Delay(30000);
            }
        }
    }
}

試したこと

AndroidManifesto.xmlに以下の行を追加しました。

<application android:label="App.Droid">
  <service android:name="com.CompanyName.App.BackgroundService"/>
  <receiver android:name="com.CompanyName.App.BootReceiver">
    <intent-filter>
      <action android:name="android.intent.action.BOOT_COMPLETED"/>
      <action android:name="android.intent.action.QUICKBOOT_POWERON"/>
      <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
      <action android:name="android.intent.action.PACKAGE_INSTALL"/>
      <action android:name="android.intent.action.PACKAGE_ADDED"/>
      <action android:name="android.intent.action.MY_PACKGE_REPLACED"/>
    </intent-filter>
  </receiver>
</application>


しかし動きませんでした。

1度位置情報の取得、送信部分のコードを削除し、それぞれの関数の1行目にConsole.WriteLineを追加し、出力を見てみました。
Console.WriteLine("OnStartCommand");
Console.WriteLine("RegisterForegroundService");
Console.WriteLine("StartBackgroundService");
Console.WriteLine("OnDestroy");
Console.WriteLine("OnReceive");
Console.WriteLine("Main");
「BackgroundThread.cs」のwhile文の中に下記行を追加しました。
Console.WriteLine("Main While");
結果としていずれも出力されませんでした。

その後、「MainActivity.cs」に下記の行を追加しました。
StartService(new Intent(this, typeof(BackgroundService)));
結果は「OnStartCommand」、「Main」、「Main While」のみ出力されました。

タスクをキルすると「Main While」の出力が止まってしまうため、目的とする動作ではありません。

どうすれば常にタスクを維持出来るでしょうか。
また、起動した瞬間やアプリを入れ替えた瞬間に動作を開始させたいのですが「BootReceiver.cs」の処理に入ってきません。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+3

直接の回答ではありませんが、実現されたい(と思われる)機能ごとに述べます。
なお、いずれもネイティブのAndroidアプリ開発の、それもかなり難易度の高い部類に入りますので、Xamarin はほとんど関係ないです。

①端末の起動をBootReceiverで受信

↑は私が書いたものですが、この2014年時点でもそれより過去とは動作が変わっており、現在もそのまま使える保証はないです。

②BackgroundServiceを起動

自力で StartService を呼び出してサービスが開始・実行されることは確認できているようですが、
タスクキラーで殺されても 「常にタスクを維持」 し続けるサービスを作ることは不可能です。

サービスが死んだら再起動させる、ことは次の情報で可能なようです(ただし2014年の情報)。

あるいは定期的に「サービスが生きているか?」の通知を何らかの方法で送信し、死んでいたら起動させるという事を行わないといけないかも知れません。
そのためには例えばサーバーから定期的にプッシュ通知を送信し、端末でそれを受信した時にサービスの死活管理を行う必要があるかもしれません。

③位置を取得

GetPositionAsync() は、一回の位置情報を取得するものなので、while ループでこれを何度も呼び出すことは、その都度GPSの再起動が行われたりしてムダが多いです。StartListeningAsync() を使った方がよいでしょう。


①も②も通常のAndroid開発でもデバッグしづらく難しいので、本当にこれらが必要ならば、ネイティブの Java or Kotlin/Android でトライ&エラーしてから Xamarin に移植した方が早いのではないかと思います。

(なお、同じような事を iOS で実装しようとした場合、iOS 固有のテクニックが必要になります。)

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/01/11 15:51

    この度は実現したいことの難しさについて教えて頂きありがとうございます。
    今回のものはXamarinという指定があるためJava or Kotlin/Androidに変更することは出来ないのでとりあえず動いているStartServiceで代用しようと思います。

    キャンセル

+2

タスク死んだときに復活するコードとして、サービスクラスに以下のような処理入れてみてはどうでしょうか。

    public override void OnTaskRemoved(Intent rootIntent)
    {
        base.OnTaskRemoved(rootIntent);
        var intent = new Intent(this, typeof(BackgroundService));
        ((AlarmManager)GetSystemService(Context.AlarmService)).Set(AlarmType.Rtc, Java.Lang.JavaSystem.CurrentTimeMillis() + 500, PendingIntent.GetService(this, 11, intent, 0));
    }

雑談

amayさんが言うようにAndroidのバックグラウンド処理はいろいろと問題が多いです。
Androidのバージョンが上がるごとに制限が厳しくなっています。
「Dozeモード」などはしっかりと調べたほうがいいです。
位置情報を定期的に取りたいということですが、
バックグラウンド中はネットワークアクセスが使えない場合もあるので注意が必要です。

多くの中華端末(HUAWEIとか)は独自の電源管理機能が実装されていて、
そもそもバックグラウンド実行が許可されていない場合があります。
そういった端末はアプリごとにバックグラウンド実行の許可をする必要があります。

iOSにはAndroidのようなバックグラウンドサービスという概念がありませんが、
位置情報に関してはバックグラウンドでの取得も可能ですが、Androidほど柔軟なことはできません。
詳しくはマニュアルを読むことをお勧めします。
https://developer.apple.com/jp/documentation/UserExperience/Conceptual/LocationAwarenessPG/CoreLocation/CoreLocation.html

上記URLが死んでました。英語ですがこちらを。
https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/LocationAwarenessPG/CoreLocation/CoreLocation.html

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/01/11 15:54

    どれをベストアンサーにするか迷いましたが実装の難しさを教えていただいた点を踏まえて別のものにしました。申し訳ありません。
    電源管理についても調査不足でした、教えて頂きありがとうございます。
    今回は実装が難しそうなのでとりあえず動いているStartServiceを使用したコートで行きたいと思います。
    iOSのマニュアルも教えて頂きありがとうございます。今後実装する際に参考にしようと思います。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.00%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる