ジオフェンスの作成と監視

ジオフェンスを使用すると、ユーザーが今いる場所の情報と、ユーザーが関心を持つであろう場所が近くにあるという情報を組み合わせて、ユーザーに知らせることができます。ユーザーが関心を持つ場所をマークするには、その場所の緯度と経度を指定します。場所の近さを調整するには、半径を追加します。緯度、経度、半径によってジオフェンスが定義され、ユーザーが関心を持つ場所を中心とする円形の領域(フェンス)が作成されます。

アクティブなジオフェンスは複数作成できます(ただし、1 つのアプリで作成できるジオフェンスはデバイス ユーザーあたり 100 個までです)。位置情報サービスに対して、ジオフェンスごとに entrance イベントと exit イベントを送信するように要求できます。また、デバイスがジオフェンス領域内にとどまる期間(滞留期間)を指定して、その期間が経過したらイベントをトリガーできます。有効期間をミリ秒単位で指定すると、ジオフェンスの存続期間を制限できます。ジオフェンスが期限切れになると、位置情報サービスが自動的にジオフェンスを削除します。

このレッスンでは、ジオフェンスを追加および削除する方法と、BroadcastReceiver を使用してジオフェンスへの出入りをリッスンする方法について説明します。

ジオフェンスの監視を設定する

ジオフェンスの監視をリクエストするには、まず、必要な権限をリクエストします。ジオフェンスを使用する場合、アプリは以下の権限をリクエストする必要があります。

詳細については、位置情報権限をリクエストする方法のガイドをご覧ください。

BroadcastReceiver を使用してジオフェンスへの出入りをリッスンする場合は、サービス名を指定して要素を追加します。この要素は、 <application> 要素の子要素でなければなりません。

<application
   android:allowBackup="true">
   ...
   <receiver android:name=".GeofenceBroadcastReceiver"/>
<application/>

位置情報 API にアクセスするには、ジオフェンス クライアントのインスタンスを作成する必要があります。クライアントを接続する方法については、以下をご覧ください。

Kotlin

lateinit var geofencingClient: GeofencingClient

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    geofencingClient = LocationServices.getGeofencingClient(this)
}

Java

private GeofencingClient geofencingClient;

@Override
public void onCreate(Bundle savedInstanceState) {
    // ...
    geofencingClient = LocationServices.getGeofencingClient(this);
}

ジオフェンスを作成して追加する

アプリでジオフェンスを作成して追加するには、ジオフェンス オブジェクトを作成するための位置情報 API のビルダークラスと、それらのオブジェクトを追加するためのコンビニエンス クラスを使用する必要があります。また、ジオフェンスへの出入りが発生したときに位置情報サービスから送信されるインテントを処理するため、PendingIntent を定義できます(このセクションで説明します)。

注: 単一ユーザー デバイスでは、1 つのアプリで作成できるジオフェンスは 100 個までです。マルチユーザー デバイスでは、1 つのアプリで作成できるジオフェンスはデバイス ユーザーあたり 100 個までです。

ジオフェンス オブジェクトを作成する

まず、 Geofence.Builder を使用してジオフェンスを作成し、ジオフェンスの半径、期間、出入りのタイプを設定します。以下に、リスト オブジェクトを設定する例を示します。

Kotlin

geofenceList.add(Geofence.Builder()
        // Set the request ID of the geofence. This is a string to identify this
        // geofence.
        .setRequestId(entry.key)

        // Set the circular region of this geofence.
        .setCircularRegion(
                entry.value.latitude,
                entry.value.longitude,
                Constants.GEOFENCE_RADIUS_IN_METERS
        )

        // Set the expiration duration of the geofence. This geofence gets automatically
        // removed after this period of time.
        .setExpirationDuration(Constants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)

        // Set the transition types of interest. Alerts are only generated for these
        // transition. We track entry and exit transitions in this sample.
        .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)

        // Create the geofence.
        .build())

Java

geofenceList.add(new Geofence.Builder()
    // Set the request ID of the geofence. This is a string to identify this
    // geofence.
    .setRequestId(entry.getKey())

    .setCircularRegion(
            entry.getValue().latitude,
            entry.getValue().longitude,
            Constants.GEOFENCE_RADIUS_IN_METERS
    )
    .setExpirationDuration(Constants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)
    .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER |
            Geofence.GEOFENCE_TRANSITION_EXIT)
    .build());

この例では、定数ファイルからデータを取得しています。実際のアプリでは、ユーザーの現在地に基づいて動的にジオフェンスを作成することもあります。

ジオフェンスと最初のトリガーを指定する

次のスニペットでは、 GeofencingRequest クラスとそのクラスにネストされた GeofencingRequestBuilder クラスを使用して監視対象のジオフェンスを指定し、関連するジオフェンス イベントをトリガーする方法を設定しています。

Kotlin

private fun getGeofencingRequest(): GeofencingRequest {
    return GeofencingRequest.Builder().apply {
        setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
        addGeofences(geofenceList)
    }.build()
}

Java

private GeofencingRequest getGeofencingRequest() {
    GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
    builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER);
    builder.addGeofences(geofenceList);
    return builder.build();
}

この例は、2 つのジオフェンス トリガーの使い方を示しています。 GEOFENCE_TRANSITION_ENTER はデバイスがジオフェンスに入ったときにトリガーされ、 GEOFENCE_TRANSITION_EXIT はデバイスがジオフェンスから出たときにトリガーされます。 INITIAL_TRIGGER_ENTER を指定すると、デバイスがすでにジオフェンス内にある場合に GEOFENCE_TRANSITION_ENTER をトリガーする必要があることが位置情報サービスに伝えられます。

多くのケースでは、 INITIAL_TRIGGER_DWELL の使用をおすすめします。そうすると、定義された期間が経過するまでユーザーがジオフェンス内にとどまったときに限り、イベントがトリガーされます。このアプローチを使用すると、デバイスが短時間にジオフェンスを出入りしたときに大量の通知が生成される「アラートスパム」を減らすことができます。ジオフェンスから最良の結果を得るためのもう 1 つの戦略は、最小半径を 100 メートルに設定することです。この方法を使用すると、一般的な Wi-Fi ネットワークの位置情報の精度を利用できるだけでなく、デバイスの消費電力も低減できます。

ジオフェンスへの出入りを処理するブロードキャスト レシーバを定義する

位置情報サービスから送信される Intent はアプリ内でさまざまなアクションをトリガーできますが、それによってアクティビティまたはフラグメントが起動されないようにする必要があります。コンポーネントはユーザーの操作によってのみ表示されるべきだからです。多くの場合、ジオフェンスへの出入りを処理する適切な手段は、BroadcastReceiver です。BroadcastReceiver は、イベント(ジオフェンスへの出入りなど)が発生したときに更新され、長期的に実行されるバックグラウンド処理を開始できます。

次のスニペットは、BroadcastReceiver を開始する PendingIntent の定義方法を示しています。

Kotlin

class MainActivity : AppCompatActivity() {

    // ...

    private val geofencePendingIntent: PendingIntent by lazy {
        val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
        // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling
        // addGeofences() and removeGeofences().
        PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
    }
}

Java

public class MainActivity extends AppCompatActivity {

    // ...

    private PendingIntent getGeofencePendingIntent() {
        // Reuse the PendingIntent if we already have it.
        if (geofencePendingIntent != null) {
            return geofencePendingIntent;
        }
        Intent intent = new Intent(this, GeofenceBroadcastReceiver.class);
        // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when
        // calling addGeofences() and removeGeofences().
        geofencePendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.
                FLAG_UPDATE_CURRENT);
        return geofencePendingIntent;
    }

ジオフェンスを追加する

ジオフェンスを追加するには、 GeofencingClient.addGeofences() メソッドを使用します。その際に、 GeofencingRequest オブジェクトと PendingIntent を指定します。次のスニペットは、結果の処理方法を示しています。

Kotlin

geofencingClient?.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run {
    addOnSuccessListener {
        // Geofences added
        // ...
    }
    addOnFailureListener {
        // Failed to add geofences
        // ...
    }
}

Java

geofencingClient.addGeofences(getGeofencingRequest(), getGeofencePendingIntent())
        .addOnSuccessListener(this, new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void aVoid) {
                // Geofences added
                // ...
            }
        })
        .addOnFailureListener(this, new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Failed to add geofences
                // ...
            }
        });

ジオフェンスへの出入りを処理する

位置情報サービスは、ユーザーがジオフェンスに出入りしたことを検出すると、ジオフェンスを追加するリクエストに挿入した PendingIntent に含まれている Intent を送信します。GeofenceBroadcastReceiver などのブロードキャスト レシーバは、Intent が呼び出されたことを検知すると、インテントからジオフェンス イベントを取得し、ジオフェンスへの出入りのタイプを確認して、どの定義済みジオフェンスがトリガーされたかを特定します。ブロードキャスト レシーバは、アプリに対してバックグラウンド処理を開始するように指示できます。また、必要な場合は、通知を出力として送信するように指示することもできます。

注: Android 8.0(API レベル 26)以上では、ジオフェンスの監視中にアプリがバックグラウンドで実行されている場合、デバイスは数分ごとにジオフェンス イベントに応答します。このような応答の制限にアプリを適応させる方法については、バックグラウンド位置情報の制限をご覧ください。

次のスニペットは、ジオフェンスへの出入りが発生したときに通知を送信する BroadcastReceiver の定義方法を示しています。ユーザーが通知をクリックすると、アプリのメイン アクティビティが表示されます。

Kotlin

class GeofenceBroadcastReceiver : BroadcastReceiver() {
    // ...
    override fun onReceive(context: Context?, intent: Intent?) {
        val geofencingEvent = GeofencingEvent.fromIntent(intent)
        if (geofencingEvent.hasError()) {
            val errorMessage = GeofenceStatusCodes
                    .getStatusCodeString(geofencingEvent.errorCode)
            Log.e(TAG, errorMessage)
            return
        }

        // Get the transition type.
        val geofenceTransition = geofencingEvent.geofenceTransition

        // Test that the reported transition was of interest.
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER |
                geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {

            // Get the geofences that were triggered. A single event can trigger
            // multiple geofences.
            val triggeringGeofences = geofencingEvent.triggeringGeofences

            // Get the transition details as a String.
            val geofenceTransitionDetails = getGeofenceTransitionDetails(
                    this,
                    geofenceTransition,
                    triggeringGeofences
            )

            // Send notification and log the transition details.
            sendNotification(geofenceTransitionDetails)
            Log.i(TAG, geofenceTransitionDetails)
        } else {
            // Log the error.
            Log.e(TAG, getString(R.string.geofence_transition_invalid_type,
                    geofenceTransition))
        }
    }
}

Java

public class GeofenceBroadcastReceiver extends BroadcastReceiver {
    // ...
    protected void onReceive(Context context, Intent intent) {
        GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
        if (geofencingEvent.hasError()) {
            String errorMessage = GeofenceStatusCodes
                    .getStatusCodeString(geofencingEvent.getErrorCode());
            Log.e(TAG, errorMessage);
            return;
        }

        // Get the transition type.
        int geofenceTransition = geofencingEvent.getGeofenceTransition();

        // Test that the reported transition was of interest.
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
                geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {

            // Get the geofences that were triggered. A single event can trigger
            // multiple geofences.
            List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();

            // Get the transition details as a String.
            String geofenceTransitionDetails = getGeofenceTransitionDetails(
                    this,
                    geofenceTransition,
                    triggeringGeofences
            );

            // Send notification and log the transition details.
            sendNotification(geofenceTransitionDetails);
            Log.i(TAG, geofenceTransitionDetails);
        } else {
            // Log the error.
            Log.e(TAG, getString(R.string.geofence_transition_invalid_type,
                    geofenceTransition));
        }
    }
}

BroadcastReceiver は、PendingIntent によってジオフェンスへの出入りイベントを検出した後、出入りのタイプを取得して、そのイベントがアプリによって通知のトリガーに使用されるイベント(この場合は、GEOFENCE_TRANSITION_ENTER または GEOFENCE_TRANSITION_EXIT)かどうかを検査します。その後、サービスは通知を送信し、ジオフェンスへの出入りの詳細をログに記録します。

ジオフェンスの監視を停止する

ジオフェンスの監視が不要になったときに停止すると、デバイスの電池使用量と CPU サイクルを節約できます。ジオフェンスの監視は、ジオフェンスの追加と削除に使用するメイン アクティビティで停止できます。ジオフェンスを削除すると、監視は直ちに停止します。API にはジオフェンスを削除するメソッドが用意されています。それらのメソッドは、リクエスト ID を指定してジオフェンスを削除するか、または特定の PendingIntent に関連付けられているジオフェンスを削除します。

次のスニペットは、PendingIntent を使用してジオフェンスを削除しています。削除後、以前に追加したジオフェンスにデバイスが出入りしたときの通知は停止されます。

Kotlin

geofencingClient?.removeGeofences(geofencePendingIntent)?.run {
    addOnSuccessListener {
        // Geofences removed
        // ...
    }
    addOnFailureListener {
        // Failed to remove geofences
        // ...
    }
}

Java

geofencingClient.removeGeofences(getGeofencePendingIntent())
        .addOnSuccessListener(this, new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void aVoid) {
                // Geofences removed
                // ...
            }
        })
        .addOnFailureListener(this, new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Failed to remove geofences
                // ...
            }
        });

ジオフェンスは、他の位置認識機能(位置情報の定期的な更新など)と組み合わせることができます。詳細については、このクラスの他のレッスンをご覧ください。

ジオフェンスに関するおすすめの方法を活用する

このセクションでは、ジオフェンスを Android 向けの位置情報 API と組み合わせて使用する場合の推奨事項について概説します。

消費電力を低減する

以下の手法により、ジオフェンスを使用するアプリの消費電力を最適化できます。

  • 通知の応答性を大きい値に設定します。そうすると、ジオフェンス アラートのレイテンシが増大するため、消費電力が抑制されます。たとえば、応答性の値を 5 分に設定すると、アプリはジオフェンスへの出入りのアラートを 5 分に 1 回だけチェックします。小さい値を設定しても、必ずしもその期間内にユーザーに通知が届くわけではありません(たとえば、値を 5 秒に設定しても、アラートを受け取るまでにそれより少し時間がかかることがあります)。

  • ユーザーが長時間過ごす場所(自宅や職場)のジオフェンス半径を大きくします。半径を大きくすることが消費電力の低減に直接つながるわけではありませんが、アプリがジオフェンスへの出入りをチェックする頻度が減るため、結果的に全体的な消費電力が低減されます。

ジオフェンスの最適な半径を選択する

最適な結果を得るには、ジオフェンスの最小半径を 100~150 メートルに設定する必要があります。Wi-Fi が利用可能な場合、位置情報の精度は通常 20~50 メートルです。屋内の位置情報を利用できる場合は、精度範囲を 5 メートルほどに狭めることができます。屋内の位置情報をジオフェンス内で利用できるかどうか不明な場合は、Wi-Fi の位置情報の精度を約 50 メートルと仮定します。

Wi-Fi の位置情報を利用できない場合(田舎をドライブしているときなど)は、位置情報の精度が低下します。その場合の精度範囲は、数百メートルから数キロメートルに広がることがあります。このような場合、半径の大きいジオフェンスを作成する必要があります。

アプリがジオフェンスを使用する理由をユーザーに説明する

ジオフェンスを使用すると、アプリはバックグラウンドで位置情報にアクセスします。したがって、アプリがユーザーにどのようなメリットを提供するかを検討する必要があります。アプリが位置情報へのアクセスを必要とする理由をユーザーに明確に説明し、ユーザーの十分な理解を得て、ユーザーに対する透明性を高めてください。

位置情報へのアクセス(ジオフェンスを含む)に関するおすすめの方法の詳細については、プライバシーに関するおすすめの方法のページをご覧ください。

出入りのタイプに滞留を使用してアラートスパムを減らす

ドライブ中など、短時間にジオフェンスを出入りして多数のアラートが送信される場合は、アラートを減らすために、出入りのタイプとして GEOFENCE_TRANSITION_ENTER ではなく GEOFENCE_TRANSITION_DWELL を使用することをおすすめします。そうすれば、ユーザーが指定された期間が経過するまでジオフェンス内にとどまったときに限り、滞留アラートが送信されます。滞留期間を選択するには、徘徊遅延を設定します。

必要な場合にのみジオフェンスを再登録する

登録されたジオフェンスは、com.google.android.gms パッケージが所有する com.google.process.location プロセスで保持されます。以下のイベントの発生後はシステムがジオフェンスを復元するため、アプリが以下のイベントを処理するために何かをする必要はありません。

  • Google Play 開発者サービスがアップグレードされた。
  • リソースの制限により、Google Play 開発者サービスがシステムによって強制終了され、再開された。
  • 位置情報プロセスがクラッシュした。

以下の場合はシステムがジオフェンスを復元できないため、以下のイベントの発生後も引き続きジオフェンスが必要な場合は、アプリでジオフェンスを再登録する必要があります。

  • デバイスが再起動された。この場合、アプリはデバイスの起動完了アクションをリッスンし、必要なジオフェンスを再起動する必要があります。
  • アプリのアンインストールと再インストールが行われた。
  • アプリのデータが消去された。
  • Google Play 開発者サービスのデータが消去された。
  • アプリが GEOFENCE_NOT_AVAILABLE アラートを受け取った。通常、このイベントは NLP(Android のネットワーク位置情報プロバイダ)が無効化された後に発生します。

ジオフェンスの entrance イベントをトラブルシューティングする

デバイスがジオフェンス内に入ったときにジオフェンスがトリガーされない( GEOFENCE_TRANSITION_ENTER アラートがトリガーされない)場合は、このガイドの説明に従ってジオフェンスが正しく登録されていることを最初に確認します。

アラートが期待どおりに動作しない場合、考えられる理由を以下に示します。

  • ジオフェンス内で正確な位置情報を利用できない。またはジオフェンスが小さすぎる。ほとんどのデバイスでは、ジオフェンス サービスはネットワーク位置情報のみを使用してジオフェンスをトリガーします。サービスがこのアプローチを使用する理由は、ネットワーク位置情報は消費電力が非常に少ないこと、不連続の位置情報を短時間で取得できること、そして最も重要な点として屋内で使用できることです。
  • デバイスで Wi-Fi が無効になっている。Wi-Fi を有効にすると、位置情報の精度が大幅に向上します。一方、Wi-Fi が無効になっていると、各種の設定(ジオフェンスの半径など)、デバイスのモデル、Android のバージョンによっては、アプリがジオフェンス アラートを受信できない場合があります。Android 4.3(API レベル 18)以降に追加された「Wi-Fi スキャン専用モード」では、ユーザーが Wi-Fi を無効にしても引き続き高精度のネットワーク位置情報を取得できます。Wi-Fi と Wi-Fi スキャン専用モードの両方が無効になっている場合は、いずれかを有効にするようユーザーに促し、そのためのショートカットを提供することをおすすめします。SettingsClient を使用すると、デバイスのシステム設定を適切に構成して、最適な位置情報を検出できます。

    注: Android 10(API レベル 29)以上をターゲットとするアプリは、システムアプリまたは Device Policy Controller(DPC)アプリでない限り、WifiManager.setEnabled() を直接呼び出すことができません。代わりに、設定パネルを使用してください。

  • ジオフェンス内に信頼できるネットワーク接続がない。信頼できるデータ接続が存在しない場合、アラートが生成されないことがあります。これは、ジオフェンス サービスが依存するネットワーク位置情報プロバイダがデータ接続を必要とするためです。
  • アラートが遅れる可能性がある。ジオフェンス サービスは位置情報を連続的にクエリしないので、アラートを受信する際にいくらかレイテンシが生じることが予想されます。レイテンシは通常 2 分未満で、デバイスが移動しているときはもっと短くなります。バックグラウンド位置情報の制限が有効になっている場合、レイテンシは平均で約 2~3 分です。デバイスが長時間同じ場所にとどまっているときは、レイテンシが最大 6 分まで増大することがあります。