ユーザーが、「徒歩」、「自動車の運転中」、「自転車の運転中」、「ランニング中」、「静止中」などを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;
}
}
いくつかの端末で試してみましたが、行動認知は、端末によって精度にムラがあるように感じました。
これから精度が上がってくることを期待したいです。