【Java】Activity Recognition Transition API をForegroundServiceを用いて利用する

ユーザーが、「徒歩」、「自動車の運転中」、「自転車の運転中」、「ランニング中」、「静止中」などをAndroidで検知したい場合は、Activity Recognition Transition API を利用するかと思います。

こちらが、公式ドキュメントです。

公式ではブロードキャストレシーバーを利用して、APIからのコールバックを受け取っています(2022/08/20時点)。

今回は、ForegroundServiceを利用して簡易的なサンプルを作ってみたいと思います。
Android SDK API レベル は、31(Android12)を想定しています。

まずは、AndroidManifest.xmlとbuild.gradle内で権限の指定の追加や、ライブラリのインポートを行います。

AndroidManifest.xml

<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />

build.gradle (dependenciesに追加)

implementation 'com.google.android.gms:play-services-location:20.0.0'

行動認知のタスクの登録やキャンセルを行うActivityRecognitionManagerクラスを作成します。
このように行う必要はないのですが、 個人的にこういったマネージャークラスを 作成するのが好きです。

ActivityRecognitionManager.java

public class ActivityRecognitionManager {

    private final ActivityRecognitionCallback mCallback;

  /**
     *  タスクが登録された、あるいはキャンセルされたときに使われるコールバック
     */
    public ActivityRecognitionManager(ActivityRecognitionCallback callback) {
        mCallback = callback;
    }

    private List<ActivityTransition> getActivityTransitions() {

        List<ActivityTransition> transitions = new ArrayList<>();

        ArrayList<Integer> activities = new ArrayList<>(Arrays.asList(
                DetectedActivity.STILL,
                DetectedActivity.WALKING,
                DetectedActivity.RUNNING,
                DetectedActivity.ON_BICYCLE,
                DetectedActivity.IN_VEHICLE));

        for (int activity : activities) {
            transitions.add(
                    new ActivityTransition.Builder()
                            .setActivityType(activity)
                            .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
                            .build());

            transitions.add(
                    new ActivityTransition.Builder()
                            .setActivityType(activity)
                            .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
                            .build());
        }

        return transitions;
    }

    public void startTask(Context context, PendingIntent pendingIntent) {

        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        
        List<ActivityTransition> transitions = getActivityTransitions();
        ActivityTransitionRequest request = new ActivityTransitionRequest(transitions);
        Task<Void> task = ActivityRecognition.getClient(context).requestActivityTransitionUpdates(request, pendingIntent);

        task.addOnSuccessListener(
                result -> {
                    // Handle success
                    mCallback.onTaskRegistered();
                }
        );

        task.addOnFailureListener(
                e -> {
                    // Handle error
                    Log.e("MY_COMPONENT", e.getMessage());
                }
        );
    }

    public void cancelTask(Context context, PendingIntent pendingIntent) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) {
            return;
        }

        Task<Void> task = ActivityRecognition.getClient(context).removeActivityTransitionUpdates(pendingIntent);

        task.addOnSuccessListener(
                result -> mCallback.onTaskCancelled()
        );

        task.addOnFailureListener(
                e -> {
                }
        );
    }

}

コールバックインターフェースです。
タスクの登録などは、Activityで行います。

ActivityRecognitionCallback.java

/**
 * Class ActivityRecognitionCallback
 */

public interface ActivityRecognitionCallback {
    void onTaskRegistered();
    void onTaskCancelled();
}

コールバックを受け取るMainActivityです。
MainActivity.java

public class MainActivity extends AppCompatActivity implements ActivityRecognitionCallback {

    public static final String TAG = "MainActivity";

    private TextView mActivityTypeTv;
    private TextView mTransitionTypeTv;
    private TextView mStatusTv;
    private Button mStartButton;
    private Button mStopButton;
    private Context mContext;
    private ActivityRecognitionManager mActivityRecognitionManager;
    private PendingIntent mTransitionPendingIntent;


    /**
     * 行動認知を受け取ったら、ActivityTransitionServiceよりブロードキャストされてUIを更新する
     */
    BroadcastReceiver uiReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String activityTypeToString = intent.getStringExtra("activityTypeToString");
            String transitionTypeToString = intent.getStringExtra("transitionTypeToString");
            String activityTypeText = getString(R.string.detective_activity_type) + " : " + activityTypeToString; // 歩いているなど
            String transitionTypeText = getString(R.string.start_or_stop) + " : " + transitionTypeToString; // 開始か終了か
            mActivityTypeTv.setText(activityTypeText);
            mTransitionTypeTv.setText(transitionTypeText);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = this;

        // UI ここから
        setTitle(R.string.main_title);
        setContentView(R.layout.activity_main);
        setLayoutProperty();
        setTextView();
        setOnListener();
        // ここまで

        mActivityRecognitionManager = new ActivityRecognitionManager(this);
        Intent transitionServiceIntent = new Intent(mContext, ActivityTransitionService.class);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            mTransitionPendingIntent = PendingIntent.getForegroundService(mContext, 0, transitionServiceIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
        } else {
            mTransitionPendingIntent = PendingIntent.getForegroundService(mContext, 0, transitionServiceIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        }

    }

    private void setOnListener() {
        mStartButton.setOnClickListener(view -> {
            Log.d(TAG, "tap start-button");

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) {
                requestPermission();
            } else {
                registerUiReceiver();
                mActivityRecognitionManager.startTask(mContext, mTransitionPendingIntent);
            }

        });

        mStopButton.setOnClickListener(view -> {
            Log.d(TAG, "tap stop-button");
            if (mTransitionPendingIntent != null) {
                mActivityRecognitionManager.cancelTask(mContext, mTransitionPendingIntent);
            }
        });

    }

    private void setTextView() {
        SharedPreferences sharedPreferences = mContext.getSharedPreferences("activity_transition_sample", Context.MODE_PRIVATE);
        String activityType = sharedPreferences.getString("activity_type", "");
        String transitionType = sharedPreferences.getString("transition_type", "");

        if (!activityType.equals("")) {
            String activityTypeText = getString(R.string.detective_activity_type) + " : " + activityType;
            mActivityTypeTv.setText(activityTypeText);
        }

        if (!transitionType.equals("")) {
            String transitionTypeText = getString(R.string.start_or_stop) + " : " + transitionType;
            mTransitionTypeTv.setText(transitionTypeText);
        }
    }

    private void setLayoutProperty() {
        mStatusTv = findViewById(R.id.statusText);
        mStartButton = findViewById(R.id.start);
        mStopButton = findViewById(R.id.stop);
        mActivityTypeTv = findViewById(R.id.activityType);
        mTransitionTypeTv = findViewById(R.id.transitionType);
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    private void requestPermission() {
        String[] permissions = new String[]{Manifest.permission.ACTIVITY_RECOGNITION};
        ActivityCompat.requestPermissions(this, permissions, 1);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            registerUiReceiver();
            mActivityRecognitionManager.startTask(mContext, mTransitionPendingIntent);
        }
    }

    protected void onDestroy() {

        if (uiReceiver != null) {
            unregisterReceiver(uiReceiver);
        }

        super.onDestroy();
    }

    /**
     * タスクを登録したらサービスを起動
     */
    private void startActivityTransitionService() {
        Intent intent = new Intent(mContext, ActivityTransitionService.class);
        mContext.startForegroundService(intent);
    }

    /**
     * タスクを削除したらサービスを停止
     */
    private void cancelActivityTransitionService() {
        Intent intent = new Intent(mContext, ActivityTransitionService.class);
        mContext.stopService(intent);
    }

    /**
     *  タスクが登録されたら呼ばれるコールバックメソッド
     */
    @Override
    public void onTaskRegistered() {
        mStatusTv.setText(R.string.detective_activity_start);
        startActivityTransitionService();
    }

    /**
     *  タスクがキャンセルされたら呼ばれるコールバックメソッド
     */
    @Override
    public void onTaskCancelled() {
        mStatusTv.setText(R.string.detective_activity_stop);
        cancelActivityTransitionService();
    }

    /**
     * 行動認知を受け取った際に、画面更新を促すために、ActivityTransitionServiceよりブロードキャストするレシーバーを登録
     */
    private void registerUiReceiver() {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(MyConstants.UPDATE_MAIN_UI);
        registerReceiver(uiReceiver, intentFilter);
    }
}

行動認知の登録を受け取ったらサービスを起動させます。

public class ActivityTransitionService extends Service {

    public static final String TAG = "ActivityTransitionService";
    private Context mContext;

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mContext = this;
        NotificationCompat.Builder builder = buildNotification();
        startForeground(334, builder.build());
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);

        Log.d(TAG, "result:"+ ActivityTransitionResult.hasResult(intent));

        if (ActivityTransitionResult.hasResult(intent)) {
            ActivityTransitionResult result = ActivityTransitionResult.extractResult(intent);
            if (result != null) {
                for (ActivityTransitionEvent event : result.getTransitionEvents()) {

                    String activityTypeToString = getActivityTypeToString(event.getActivityType());
                    String transitionTypeToString = getTransitionTypeToString(event.getTransitionType());

                    Log.d(TAG, activityTypeToString);
                    Log.d(TAG, transitionTypeToString);

                    saveActivityTransition(activityTypeToString, transitionTypeToString);
                    sendToMainActivity(activityTypeToString, transitionTypeToString);

                }
            }
        }

        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        stopSelf();
    }

    /**
     * MainActivityのUIへ画面更新を促すためにブロードキャスト
     * @param activityTypeToString 
     * @param transitionTypeToString
     */
    private void sendToMainActivity(String activityTypeToString , String transitionTypeToString) {
        Intent broadcastIntent = new Intent();
        broadcastIntent.putExtra("activityTypeToString", activityTypeToString);
        broadcastIntent.putExtra("transitionTypeToString", transitionTypeToString);
        broadcastIntent.setAction(MyConstants.UPDATE_MAIN_UI);
        mContext.sendBroadcast(broadcastIntent);
    }

    /**
     * 受け取ったactivityTypeとtransitionTypeを保存しておく
     * @param activityTypeToString
     * @param transitionTypeToString
     */
    private void saveActivityTransition(String activityTypeToString, String transitionTypeToString) {
        SharedPreferences sharedPreferences = mContext.getSharedPreferences("activity_transition_sample", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString("activity_type", activityTypeToString);
        editor.putString("transition_type", transitionTypeToString);
        editor.apply();

        DataBaseHelper helper = new DataBaseHelper(mContext);
        helper.saveActivityRecognition(activityTypeToString, transitionTypeToString);
    }

    /**
     * アクティビティタイプを「走っている」などの日本語に変換
     * @param activityType
     * @return String
     */
    private String getActivityTypeToString(int activityType) {

        String activityTypeToString;

        switch (activityType) {
            case DetectedActivity.IN_VEHICLE:
                activityTypeToString = mContext.getString(R.string.in_vehicle);
                break;
            case DetectedActivity.ON_BICYCLE:
                activityTypeToString = mContext.getString(R.string.on_bicycle);
                break;
            case DetectedActivity.RUNNING:
                activityTypeToString = mContext.getString(R.string.running);
                break;
            case DetectedActivity.STILL:
                activityTypeToString = mContext.getString(R.string.still);
                break;
            case DetectedActivity.WALKING:
                activityTypeToString = mContext.getString(R.string.walking);
                break;
            default:
                activityTypeToString = mContext.getString(R.string.unknown);
                break;
        }

        return activityTypeToString;
    }

    /**
     * 「開始」か「終了」という日本語に変換
     * @param transitionType
     * @return String
     */
    private String getTransitionTypeToString(int transitionType) {

        if (transitionType == ActivityTransition.ACTIVITY_TRANSITION_ENTER) {
            return mContext.getString(R.string.enter);
        } else {
            return mContext.getString(R.string.exit);
        }

    }

    /**
     * 通知を作成
     * @return NotificationCompat.Builder
     */
    public NotificationCompat.Builder buildNotification() {

        NotificationCompat.Builder builder;

        String title = mContext.getString(R.string.app_name);
        NotificationChannel channel;

        String message = mContext.getString(R.string.detective_activity_start);

        channel = new NotificationChannel("ActivityRecognition", mContext.getString(R.string.detective_activity_notification), NotificationManager.IMPORTANCE_DEFAULT);
        channel.setDescription(mContext.getString(R.string.detective_activity_notification));

        channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
        channel.enableVibration(true);
        channel.enableLights(true);
        channel.setShowBadge(false);

        NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);

        notificationManager.createNotificationChannel(channel);

        int icon = R.drawable.ic_launcher_foreground;
        String color = "#31da61";

        builder = new NotificationCompat.Builder(mContext, "ActivityRecognition")
                .setContentTitle(title)
                .setSmallIcon(icon)
                .setOngoing(true)
                .setStyle(new NotificationCompat.BigTextStyle().bigText(message))
                .setColor(Color.parseColor(color));

        Intent MainActivityIntent = new Intent(mContext, MainActivity.class);

        PendingIntent pendingIntent;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            pendingIntent = PendingIntent.getActivity(mContext, 0, MainActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
        } else {
            pendingIntent = PendingIntent.getActivity(mContext, 0, MainActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT  | PendingIntent.FLAG_IMMUTABLE);
        }
        builder.setContentIntent(pendingIntent);

        return builder;
    }
}

いくつかの端末で試してみましたが、行動認知は、端末によって精度にムラがあるように感じました。
これから精度が上がってくることを期待したいです。

【PHP】ミリ秒を含む秒数まで同じ日付を比較すると意図しないものになった

結果を知ってしまえば、大した話ではないのですが…

比較に、 strtotime関数を使ってませんか?

strtotime (PHP公式)

公式にもある通り、「英文形式の日付を Unix タイムスタンプに変換する」のがstrtotime関数です。

Unixタイムスタンプは、「1970 年 1 月 1 日 00:00:00 UTC からの経過数」なので、までしか扱いません。
ですので、以下のような場合は、falseになってしまいます。

$a = '2022-09-01 15:52:04.336';
$b = '2022-09-01 15:52:04.975';

// どちらも 1662015124 で同じ値
if (strtotime($a) < strtotime($b)) {
    var_dump(true);
} else {
    // こっちに来る
    var_dump(false); 
}

では、ミリ秒で比較したいときはどうすればいいかですが、
DateTimeImmutableもしくは DateTime オブジェクトをそれぞれ作成して比較するといいと思います。

$a = '2022-09-01 15:52:04.336';
$b = '2022-09-01 15:52:04.975';

try {
    $aObject = new DateTimeImmutable($a, new DateTimeZone('Asia/Tokyo'));
    $bObject = new DateTimeImmutable($b, new DateTimeZone('Asia/Tokyo'));

    if ($aObject < $bObject) {
        // こっちに来る
        var_dump(true);
    } else {
        var_dump(false);
    }

} catch (Exception $e) {
    // 例外処理
}

それぞれ、DateTimeImmutableオブジェクトを作成してそのままオブジェクト同士を等号や不等号で比較すればいいだけいいので個人的にはこちらの方が見やすいですね。

今は日付操作はDateTime系のオブジェクトを使うのが一般的ですので、今はあまりstrtotime関数は使われていないかと思いますが、長く続いているシステムだと残っているかもしれません。

Scala java.lang.UnsupportedOperationExceptionが起きる大体の原因

Scalaをやるのは2年ぶりの私です。

で、このエラーが頻発して悩んでいます。(>_<)

大体は、下記が理由で起こります。

あなたが触っているその変数、Immutableじゃない??

Scalaでは List とかもImmutableです。

val で宣言された変数はImmutableです。


ポエムみたいな投稿ではありますが、自分用のメモとしても書いておきます。

スカラベ

繰り返しを行う setInterval() の中でのエラー処理 サンプルコード有

javascriptで〇秒おきに〇〇を行うなどをするにはsetInterval()という関数を使いますが、これを実行したときのエラー処理がちょっと難しいので書いておきます。

まずは普通のsetInterval()

まずは普通のsetInterval()ですが、次のように普通にやってみます。

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <title></title>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <meta name="robots" content="noindex, nofollow">
  <meta name="googlebot" content="noindex, nofollow">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://unpkg.com/vue@2.5.17"></script>

</head>
<body>

<div id="app">


</div>
<script>
  function helloWorld() {
    console.log("こんにちは、");
    try {
      setInterval(function(){
        console.log("日本");
      },1000);
    }
    catch (err) {
      console.log("なんかしらのエラー処理");
    }
    console.log("世界");
  }

  helloWorld();


</script>
</body>
</html>

これを実行して、consoleを見ると、次のように出力されています。(画面には何も出力されませんのでご注意を)

私はFirefoxを使っているのでFirefoxの開発者コンソールの画面ですが、次のようになっています。

最初に「こんにちは」と「世界」が出力され、次に一秒ごとに「日本」が出力されています。最近の開発者コンソールは、同じ出力をまとめてくれるので、上記のスクショだと「日本」の文字の横に「131」という文字が青地に白で書いてありますが、これが連続して出力された回数です。

わざとエラーを起こしてみる

さて、これだとエラーが起こりえないので、わざとエラーを起こしてみます。

<!-- HTML 部分は省略 -->

    function helloWorld() {
        console.log("こんにちは、");
        try {
            setInterval(function () {
                throw new Error("エラーを投げるよ");
                console.log("日本");
            }, 1000);
        } catch (err) {
            console.log("なんかしらのエラー処理");
        }
        console.log("世界");
    }

    helloWorld();

最初に実行したときは、「こんにちは」「世界」が出力され、そのあとのsetInterval() 内での処理はUncaught Errorが発生して、ストップしてしまいます。
1秒間隔での処理もストップしてしまいます。

なぜ普通のtry~catch ではダメなのか

catch 節に行かないのはどうしてでしょうか。

javascript がエラーをキャッチできるのは、最初に setInterval を起動したときだけです。

「ハ?setInterval は1秒ごとに実行されてるじゃんかよ~?!」

と思うと思います。私も最初はこの説明を聞いても

「な… 何を言っているのかわからねーと思うが…おれも何をされたのか…わからなかった…」

というポルナレフ状態でした。

まぁ、setInterval を起動するのが元のtry~catch節です。setIntervalの中身の実行が1秒ごとに行われるときには、元の try~catch節 の知りえない世界でやられているので 元の try~catch節 ではキャッチできないということですね。

たとえ話でいうと、私がペットの猿のウキ吉に

「これから出かけるので、5分ごとに玄関に誰か来ないか見張りに行ってね。」

と命令したとしましょう。これが最初の実行で、setIntervalの起動です。この時点ではエラーが起こっていないということです。

しかし、私はそのあと出かけてしまうので、ウキ吉がちゃんと5分ごとに見張りをしているかは私にはわかりません…。

ウキ吉の行動がsetInterval の中のコードということになります。

setInterval()内でのエラーをキャッチ するサンプルコード

なので、setInterval() 内でのエラーをキャッチするためには、 setInterval() 内で エラー処理をする必要があります。

私はエラーがあった時の処理がわかりやすいほうがいいので、下記のようにPromiseを使う方法が好きです。

下記のサンプルでは、1秒ごとに2分の1の確率で、猿の留守番が失敗することになります。

しかし、猿の留守番は見張られているので、成功・失敗により処理がわかれます。ちゃんと留守番をしているとバナナがもらえて、留守番をしていないと叱られるということです。

<!-- HTML部分は省略 -->
    function rusuban() {
        console.log('ウキ吉の留守番開始')
        setInterval(
            function () {
                console.log('時間が経過')
                monkeyWatch()
                    .then(
                        function (msg) {
                            console.log(msg)
                            console.log('エライ!ご褒美のバナナだよ')
                        }
                    )
                    .catch(
                        function (err) {
                            console.log(err)
                            console.log('こらっ ダメでしょ!')
                        }
                    )
            },
            1000)

        function monkeyWatch() {
            return new Promise((resolve, reject) => {
                if (Math.random() > 0.5) {
                    console.log('ちゃんと留守番している')
                    resolve('成功')
                } else {
                    console.log('留守番していない!')
                    reject('失敗')
                }
            })
        }
    }

    rusuban();

上記の実行結果は下記のようになります。

失敗しても、1秒後には次のターンが来ます。

ちょっとコードが長くなっちゃうのが欠点ではありますね💦


Android Jetpack Roomを利用してみる【Java】

公式サイト はこちら

公式サイトに記載があるように、ローカル データベースにデータを保存する場合は、「 Room を使用することを強くおすすめします」とのことだったので、使ってみます。

使ってみて忘れないために本記事を残しておきたいと思います。
まずは公式ドキュメントを見て理解します。

Roomのアーキテクチャの図(公式より)

主要コンポーネント(公式より)

データベースクラス
データベースを保持し、アプリの永続データに対する基礎的な接続のメイン アクセスポイントとして機能します。  

データエンティティ
アプリのデータベースのテーブルを表します。  

データアクセスオブジェクト(DAO)
アプリがデータベースのデータのクエリ、更新、挿入、削除に使用できるメソッドを提供します。

なるほど…

……

私が Androidの公式ドキュメントを読んだだけで理解することは、ほぼないので実際に使ってみたいと思います。

サンプルコード

ライブラリのインポート

build.gradle の dependencies に記載。

build.gradle

def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"

テーブル構成

今回は、以下のような緯度経度が入った coordinates テーブルを作成していきます。

カラム名内容
idintID
latitudedouble緯度
longitudedouble経度
created_atString作成日

主要コンポーネント

コンポーネントクラス名役割
データベースクラス AppData アクセスポイント
データエンティティCoordinateテーブル構成
データアクセスオブジェクト(DAO)CoordinateDao データベースの操作、クエリ置き場のようなイメージ

またデータの取得やインサートなどの操作を命令する CoordinateHelperクラス も作成します。

Database

entitiesの指定と、versionの指定などを行います。
見よう見まねで使っている感じが強いです。

AppData.java

@Database(entities = {Coordinate.class}, version = 1, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
    //DAO用メソッド
    public abstract CoordinateDao coordinateDao();
}

Entity

上述のテーブル構成はこちらに記載します。

テーブル名は、coordinates
プライマリーキーは id に設定しており、アノテーションを設定します。
autoGenerate は、インサート時に自動で生成されます。
auto_incrementと同じようなイメージでしょうか。
カラム名は特に指定がなければプロパティと同じになります。
ただし、カラム名はスネークケース、javaではプロパティ名をキャメルケースで書くのが一般的と思いますので、カラム名とプロパティ名を分けたい場合は、 ColumnInfo で設定します。

Coordinate.java

@Entity(tableName = "coordinates")
public class Coordinate {

    /**
     * 定義するカラム
     */
    @PrimaryKey(autoGenerate = true)
    public int id;
    public double latitude;
    public double longitude;

    // カラム名とプロパティ名が異なる場合は、ColumnInfoで設定する
    @ColumnInfo(name = "created_at")
    public String createdAt;

    /**
     * コンストラクタ
     * @param latitude 緯度
     * @param longitude 経度
     * @param createdAt 日付
     */
    public Coordinate(double latitude, double longitude, String createdAt) {
        this.latitude = latitude;
        this.longitude = longitude;
        this.createdAt = createdAt;
    }

    public int getId() {
        return this.id;
    }

    public double getLatitude() {
        return this.latitude;
    }

    public double getLongitude() {
        return this.longitude;
    }

    public String getCreatedAt() {
        return this.createdAt;
    }

}

DAO

上述しましたが、クエリ置き場のようなイメージです。
insert、update、deleteはアノテーションさえ付ければsql文を書く必要がありません。

CoordinateDao.java

@Dao
public interface CoordinateDao {
    /**
     * 全データを昇順で取得
     * @return リスト
     */
    @Query("SELECT * FROM coordinates ORDER BY id ASC")
    List<Coordinate> getCoordinateList();

    /**
     * データを追加
     * @param coordinate 新規データ
     */
    @Insert
    void insert(Coordinate coordinate);

    /**
     * データを更新
     * @param coordinate 更新データ
     */
    @Update
    void update(Coordinate coordinate);

    /**
     * データを削除
     * @param coordinate 削除データ
     */
    @Delete
    void delete(Coordinate coordinate);
}

命令を下すHelperクラス

Roomでは、メインスレッドでクエリを発行しようとするとエラーを吐きますので、別スレッド(非同期処理)で行う必要があります。
サンプルでは、ExecutorService を利用しています。

CoordinateHelper.java

public class CoordinateHelper {

    public static final String DB_NAME = "coordinate.db";
    private final CoordinateDao mDao;
    private List<Coordinate> mCoordinateList;

    /**
     *  コンストラクタ
     */
    public CoordinateHelper(Context context) {
        AppDatabase db = Room.databaseBuilder(context, AppDatabase.class, DB_NAME).build();
        mDao = db.coordinateDao();
    }

    /**
     *  DBからデータ取得してセット
     */
    public void setCoordinateLocationList() {
        // Roomは非同期でアクセスしないと、エラーを出すので注意
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    mCoordinateList = mDao.getCoordinateList();
                    // UI変更など
                    
                } catch (Exception e) {
                    // エラー処理
                }
            }
        });
    }

    /**
     * DBへデータ追加
     */
    public void insert(Coordinate coordinate){

        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    mDao.insert(coordinate);
                } catch (Exception e) {
                    // エラー処理
                }
            }
        });
    }

    /**
     * DBのデータを削除
     */
    public void delete(Coordinate coordinate){
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    mDao.delete(coordinate);
                } catch (Exception e) {
                    // エラー処理
                }
            }
        });
    }

}

アクティビティから呼び出し

データの追加例です。
Coordinateオブジェクトを生成して、命令を下すHelperオブジェクトにデータベースにデータを追加してもらっています。

SampleActivity.java

Coordinate coordinate = new Coordinate(latitude, longitude, createdAt);
CoordinateHelper coordinateHelper = new CoordinateHelper(context);
coordinateHelper.insert(coordinate); // データの追加

感想

軽く触ってみた感じだと、結構簡単にデータ操作が行えたなっていう印象ですね。
非同期処理が強いられるので、クエリ後の結果を使って操作する部分は、初学者にとっては難しそうだと感じました。
強くおすすめしているぐらいなので、これからはRoomを使うのが当たり前になるのでしょうか。