diff --git a/app/src/main/java/com/example/tampopo_client/models/FriendRequest.java b/app/src/main/java/com/example/tampopo_client/models/FriendRequest.java index 939f0b7..c38d042 100644 --- a/app/src/main/java/com/example/tampopo_client/models/FriendRequest.java +++ b/app/src/main/java/com/example/tampopo_client/models/FriendRequest.java @@ -1,10 +1,18 @@ package com.example.tampopo_client.models; +import com.fasterxml.jackson.annotation.JsonProperty; + public class FriendRequest { + @JsonProperty("friend-request-id") private Integer id; + @JsonProperty("sender-id") private String senderId; + @JsonProperty("receiver-id") private String receiverId; + public FriendRequest() { + } + public FriendRequest(String senderId, String receiverId) { this.senderId = senderId; this.receiverId = receiverId; @@ -33,4 +41,4 @@ public void setReceiverId(String receiverId) { this.receiverId = receiverId; } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/tampopo_client/viewmodels/ActivityViewModel.java b/app/src/main/java/com/example/tampopo_client/viewmodels/ActivityViewModel.java index 0da1699..07c7246 100644 --- a/app/src/main/java/com/example/tampopo_client/viewmodels/ActivityViewModel.java +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/ActivityViewModel.java @@ -13,13 +13,9 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.SortedSet; -import java.util.TreeSet; import retrofit2.Call; import retrofit2.Callback; @@ -31,16 +27,14 @@ private final ActivitiesResource activitiesResource; private final UserResource userResource; - private final MutableLiveData myLatestActivityLiveData; - private final Map>> friendToActivitiesLiveData; - private final MutableLiveData> friendUserIdsLiveData; - private final MutableLiveData> allFriendsLatestActivitiesLiveData; + private final MutableLiveData myLatestActivityLiveData = new MutableLiveData<>(); + private final MutableLiveData> activitiesLiveData = new MutableLiveData<>(new HashMap<>()); + private final MutableLiveData> friendUserIdsLiveData = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> allFriendsLatestActivitiesLiveData = new MutableLiveData<>(new ArrayList<>()); private final String myUserId; private final String myToken; - private final List userActivityStatusChangeListeners; - public ActivityViewModel(String myUserId, String myToken) { this.myUserId = myUserId; this.myToken = myToken; @@ -48,19 +42,11 @@ final Retrofit retrofit = new Retrofit.Builder().baseUrl("http://nitta-lab-www.is.konan-u.ac.jp/tampopo/").addConverterFactory(JacksonConverterFactory.create()).build(); activitiesResource = retrofit.create(ActivitiesResource.class); userResource = retrofit.create(UserResource.class); - - friendToActivitiesLiveData = new HashMap<>(); - friendUserIdsLiveData = new MutableLiveData<>(new ArrayList<>()); - myLatestActivityLiveData = new MutableLiveData<>(null); - allFriendsLatestActivitiesLiveData = new MutableLiveData<>(new ArrayList<>()); - - userActivityStatusChangeListeners = new ArrayList<>(); } @Override public Runnable onUpdate() { return () -> { - // 1. まず自分の最新アクティビティを取得 pullLatestActivity(myUserId, activitiesResource, new ActivityFetchCallback() { @Override public void onSuccess(Activity activity) { @@ -70,29 +56,24 @@ public void onFailure(Throwable throwable) {} }); - // 2. フレンドリストを最新化し、そのコールバックで各フレンドのアクティビティを取得する pullLatestFriendUserIds(myUserId, myToken); }; } private void pullLatestFriendUserIds(String userId, String token) { - Call> call = userResource.getFriends(userId, token); - call.enqueue(new Callback>() { + userResource.getFriends(userId, token).enqueue(new Callback>() { @Override public void onResponse(@NonNull Call> call, @NonNull Response> response) { if (response.isSuccessful() && response.body() != null) { - List friends = response.body(); - friendUserIdsLiveData.postValue(friends); - - // フレンド一人一人の最新アクティビティを取得 - for (String friendId : friends) { + friendUserIdsLiveData.postValue(response.body()); + for (String friendId : response.body()) { fetchAndNotifyFriendActivity(friendId); } } } @Override public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - Log.e("ActivityVM", "Failed to fetch friends list", t); + Log.e("ActivityVM", "Failed to fetch friends", t); } }); } @@ -101,49 +82,34 @@ pullLatestActivity(friendId, activitiesResource, new ActivityFetchCallback() { @Override public void onSuccess(Activity activity) { - // 個別のLiveData更新 - updateFriendToActivitiesLiveData(activity, friendId); - - // 全体管理用のリストも更新して通知 + Map currentMap = activitiesLiveData.getValue(); + if (currentMap == null) currentMap = new HashMap<>(); + currentMap.put(friendId, activity); + activitiesLiveData.postValue(currentMap); + List currentList = allFriendsLatestActivitiesLiveData.getValue(); if (currentList == null) currentList = new ArrayList<>(); - else currentList = new ArrayList<>(currentList); // 変更検知のためにコピー - + else currentList = new ArrayList<>(currentList); currentList.removeIf(a -> a.getUserId().equals(friendId)); currentList.add(activity); - - // 更新時間順にソート(必要なら) allFriendsLatestActivitiesLiveData.postValue(currentList); } @Override - public void onFailure(Throwable throwable) { - Log.e("ActivityVM", "Failed to fetch activity for " + friendId); - } + public void onFailure(Throwable throwable) {} }); } - private void updateFriendToActivitiesLiveData(Activity latestActivity, String userId) { - MutableLiveData> userActivitiesLiveData = friendToActivitiesLiveData.computeIfAbsent(userId, k -> new MutableLiveData<>(new ArrayList<>())); - List current = userActivitiesLiveData.getValue(); - if (current == null || current.isEmpty() || !current.get(0).getText().equals(latestActivity.getText())) { - userActivitiesLiveData.postValue(List.of(latestActivity)); - } - } - private void pullLatestActivity(String userId, ActivitiesResource resource, ActivityFetchCallback callback) { - Call> call = resource.getActivities(userId, "LATEST"); - call.enqueue(new Callback>() { + // API戻り値が List であることを ActivitiesResource.java で確認済み + resource.getActivities(userId, "LATEST").enqueue(new Callback>() { @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { - if (response.isSuccessful() && response.body() != null) { - Collection activities = response.body().values(); - if (!activities.isEmpty()) { - callback.onSuccess(activities.iterator().next()); - } + public void onResponse(@NonNull Call> call, @NonNull Response> response) { + if (response.isSuccessful() && response.body() != null && !response.body().isEmpty()) { + callback.onSuccess(response.body().get(0)); } } @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { callback.onFailure(t); } }); @@ -154,7 +120,6 @@ @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { - // 追加後、即座に自分を更新するために再取得 pullLatestActivity(userId, activitiesResource, new ActivityFetchCallback() { @Override public void onSuccess(Activity activity) { myLatestActivityLiveData.postValue(activity); } @@ -168,25 +133,13 @@ }); } - public MutableLiveData getMyLatestActivityLiveData() { return myLatestActivityLiveData; } - public MutableLiveData> getActivitiesLiveDataFromUserId(String userId) { - return friendToActivitiesLiveData.computeIfAbsent(userId, k -> new MutableLiveData<>()); - } + public MutableLiveData> getActivitiesLiveData() { return activitiesLiveData; } public MutableLiveData> getFriendUserIdsLiveData() { return friendUserIdsLiveData; } public MutableLiveData> getAllFriendsLatestActivitiesLiveData() { return allFriendsLatestActivitiesLiveData; } - public String getMyUserId() { return myUserId; } - public String getMyToken() { return myToken; } - - private static LocalDateTime getDateTimeFromString(String dateTime) { - try { - return LocalDateTime.parse(dateTime, DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm")); - } catch (Exception e) { - return LocalDateTime.now(); - } - } + public MutableLiveData getMyLatestActivityLiveData() { return myLatestActivityLiveData; } private interface ActivityFetchCallback { void onSuccess(Activity activity); void onFailure(Throwable throwable); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/tampopo_client/viewmodels/ActivityViewModelFactory.java b/app/src/main/java/com/example/tampopo_client/viewmodels/ActivityViewModelFactory.java new file mode 100644 index 0000000..bee346d --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/ActivityViewModelFactory.java @@ -0,0 +1,25 @@ +package com.example.tampopo_client.viewmodels; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +public class ActivityViewModelFactory implements ViewModelProvider.Factory { + private final String userId; + private final String token; + + public ActivityViewModelFactory(String userId, String token) { + this.userId = userId; + this.token = token; + } + + @NonNull + @Override + @SuppressWarnings("unchecked") + public T create(@NonNull Class modelClass) { + if (modelClass.isAssignableFrom(ActivityViewModel.class)) { + return (T) new ActivityViewModel(userId, token); + } + throw new IllegalArgumentException("Unknown ViewModel class"); + } +} diff --git a/app/src/main/java/com/example/tampopo_client/viewmodels/RealTimeViewModel.java b/app/src/main/java/com/example/tampopo_client/viewmodels/RealTimeViewModel.java new file mode 100644 index 0000000..ad04946 --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/RealTimeViewModel.java @@ -0,0 +1,40 @@ +package com.example.tampopo_client.viewmodels; + +import android.os.Handler; +import android.os.Looper; +import androidx.lifecycle.ViewModel; + +public abstract class RealTimeViewModel extends ViewModel { + private final Handler handler = new Handler(Looper.getMainLooper()); + private Runnable updateTask; + private boolean isRunning = false; + + public abstract Runnable onUpdate(); + + public void startUpdating(long intervalSeconds) { + if (isRunning) return; + isRunning = true; + updateTask = new Runnable() { + @Override + public void run() { + if (!isRunning) return; + onUpdate().run(); + handler.postDelayed(this, intervalSeconds * 1000); + } + }; + handler.post(updateTask); + } + + public void stopUpdating() { + isRunning = false; + if (updateTask != null) { + handler.removeCallbacks(updateTask); + } + } + + @Override + protected void onCleared() { + super.onCleared(); + stopUpdating(); + } +} diff --git a/app/src/main/java/com/example/tampopo_client/viewmodels/UserActivityStatusChangeListener.java b/app/src/main/java/com/example/tampopo_client/viewmodels/UserActivityStatusChangeListener.java new file mode 100644 index 0000000..9e39925 --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/UserActivityStatusChangeListener.java @@ -0,0 +1,9 @@ +package com.example.tampopo_client.viewmodels; + +public interface UserActivityStatusChangeListener { + enum Status { + ACTIVE, + INACTIVE + } + void onUserStatusChanged(String userId, Status status); +} diff --git a/app/src/main/java/com/example/tampopo_client/views/MainActivity.java b/app/src/main/java/com/example/tampopo_client/views/MainActivity.java index 14bf304..3a854e6 100644 --- a/app/src/main/java/com/example/tampopo_client/views/MainActivity.java +++ b/app/src/main/java/com/example/tampopo_client/views/MainActivity.java @@ -1,7 +1,14 @@ package com.example.tampopo_client.views; +import android.app.Dialog; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.ArrayAdapter; @@ -9,12 +16,18 @@ import android.widget.EditText; import android.widget.GridView; import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.Toast; import androidx.activity.EdgeToEdge; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; @@ -26,12 +39,24 @@ import com.example.tampopo_client.Tampopo; import com.example.tampopo_client.models.Activity; import com.example.tampopo_client.viewmodels.ActivityViewModel; +import com.example.tampopo_client.viewmodels.ChatViewModelFactory; import com.example.tampopo_client.viewmodels.UserViewModel; +import com.google.android.material.imageview.ShapeableImageView; +import com.example.tampopo_client.viewmodels.ActivityViewModelFactory; +import com.example.tampopo_client.viewmodels.ChatViewModel; +import com.example.tampopo_client.viewmodels.NotificationListener; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; -public class MainActivity extends AppCompatActivity { +import android.util.TypedValue; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; + +public class MainActivity extends AppCompatActivity implements NotificationListener { private EditText editMessage; private ImageButton sendButton; @@ -41,10 +66,20 @@ //アクティビティの選択肢 private String[] words = {"ひまnow", "あそぼ!", "そろそろ会いたない〜?", "勉強なう", "電話しよ~", "お風呂入ってくる~", "今暇だよー!", "いそがしい~!!"}; private Button openDialogButton; + private Map userViews = new HashMap<>(); + private int[] marginTopInDp = {90,100,300,450,480,310,1000}; + private int[] marginStartInDp = {0,250,0,90,200,280,1000}; + private int i = 0; ActivityViewModel activityViewModel; + UserViewModel userViewModel; Tampopo tampopo; + //追加しました! + private ChatViewModel chatViewModel; + private final List recentUpdatedFriends = new ArrayList<>(); // 最新6人 + + @Override protected void onCreate(Bundle savedInstanceState) { @@ -57,6 +92,23 @@ return insets; }); + //疑似的な通知のためにchatViewModelを定義する + chatViewModel = new ViewModelProvider(this).get(ChatViewModel.class); + chatViewModel.addNotificationListener(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + this, + new String[]{android.Manifest.permission.POST_NOTIFICATIONS}, + 1001 + ); + } + } + + // TODO: CHANGE + userViewModel = new ViewModelProvider(this).get(UserViewModel.class); + //メイン画面からフレンド一覧画面への遷移 ImageButton friendButton = (ImageButton) findViewById(R.id.friend); friendButton.setOnClickListener(new View.OnClickListener() { @@ -66,54 +118,76 @@ } }); - //activityViewModelを宣言する - activityViewModel = new ViewModelProvider(this).get(ActivityViewModel.class); //tampopoを宣言する tampopo = (Tampopo) getApplication(); + //activityViewModelを宣言する + ActivityViewModelFactory factory = new ActivityViewModelFactory(tampopo.getUserId(), tampopo.getToken()); // Factoryを使って、引数をコンストラクタにわたしつつViewModelを作成 + activityViewModel = new ViewModelProvider(this, factory).get(ActivityViewModel.class); -// MutableLiveData>friendUserIdsLiveDate = activityViewModel.getFriendUserIdsLiveData(); -// friendUserIdsLiveDate.observe(this, new Observer>() { -// @Override -// public void onChanged(List friendLive) { -// if(friendLive != null){ -// friendIds = friendLive; -// updateActivityView(activityViewModel.getActivitiesLiveData().getValue()); -// } -// } -// }); + //追加しました!!!!!!!!!!! + // ChatViewModelを初期化する + ChatViewModelFactory factory1 = new ChatViewModelFactory(tampopo.getUserId(), tampopo.getToken(), tampopo.getChatroomId()); + chatViewModel = new ViewModelProvider(this, factory1).get(ChatViewModel.class); - MutableLiveData> activitiesLiveData = activityViewModel.getActivitiesLiveData(); - activitiesLiveData.observe(this, new Observer>() { + activityViewModel.getAllFriendsLatestActivitiesLiveData().observe(this, new Observer>() { @Override - public void onChanged(Map activities) { + public void onChanged(List activities) { updateActivityView(activities); } }); - //メイン画面から通知一覧画面への遷移 -// ImageButton notificationButton = (ImageButton)findViewById(R.id.notification); -// notificationButton.setOnClickListener(new View.OnClickListener() { -// public void onClick(View v) { -// Intent intent = new Intent(MainActivity.this,notificationActivity. class); -// startActivity(intent); -// } -// }); - //メイン画面から設定画面への遷移 -// ImageButton settingButton = (ImageButton)findViewById(R.id.setting); -// settingButton.setOnClickListener(new View.OnClickListener() { -// public void onClick(View v) { -// Intent intent = new Intent(MainActivity.this,settingActivity. class); -// startActivity(intent); -// } -// }); + ImageButton settingButton = (ImageButton) findViewById(R.id.setting); + settingButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Intent intent = new Intent(MainActivity.this, SettingActivity.class); + startActivity(intent); + } + }); messageList = findViewById(R.id.messageList); // ボタンを押すとダイアログ表示(トリガー用) openDialogButton = findViewById(R.id.openDialogButton); openDialogButton.setOnClickListener(v -> showInputDialog()); + + } + + @Override + protected void onStart() { + super.onStart(); + + if (activityViewModel != null) { + activityViewModel.startUpdating(1L); + } + if (chatViewModel != null) { + chatViewModel.startUpdating(1L); + } + } + + @Override + protected void onStop() { + super.onStop(); + + if (activityViewModel != null) { + activityViewModel.stopUpdating(); + } + if (chatViewModel != null) { + chatViewModel.stopUpdating(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (activityViewModel != null) { + activityViewModel.stopUpdating(); + } + if (chatViewModel != null) { + chatViewModel.stopUpdating(); + } } private void showInputDialog() { @@ -171,27 +245,117 @@ dialog.show(); } - private void updateActivityView(Map activities) { - TextView comment = this.findViewById(R.id.friend01_comment); - for (Activity ac: activities.values()) { - comment.setText(ac.getText()); + //natty 仮のフレンド情報 + private int getUserIconResource(String userId) { + switch (userId) { + case "user01": + return R.drawable.friend01_icon; + case "user02": + return R.drawable.friend04_icon; + case "user03": + return R.drawable.friend03_icon; + default: + return R.drawable.default_icon; } - -// if (activities == null || friendIds == null) return; -// -// messageList.removeAllViews(); // 表示をリセット -// -// for (String userId : friendIds) { -// Activity activity = activities.get(userId); -// if (activity != null) { -// TextView textView = new TextView(this); -// textView.setText(activity.getText()); -// textView.setTextSize(16); -// textView.setPadding(16, 16, 16, 16); -// // 必要に応じてユーザー名なども表示できる -// messageList.addView(textView); -// } -// } } + private void updateActivityView(List activities) { + ConstraintLayout layout = findViewById(R.id.main); + + i = 0; + for (Activity activity : activities) { + String friendId = activity.getUserId(); + + FriendIconView userView = userViews.get(friendId); + + if (userView == null) { + // 新しいユーザなので、アイコン+コメントを作成 + userView = new FriendIconView(this, friendId, "", chatViewModel); + userView.setId(View.generateViewId()); + + ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.WRAP_CONTENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ); + userView.setLayoutParams(params); + + layout.addView(userView); + userViews.put(friendId, userView); + } + + userView.setComment(activity.getText()); + + ConstraintSet set = new ConstraintSet(); + set.clone(layout); + int marginTopInPx = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + marginTopInDp[i], + getResources().getDisplayMetrics() + ); + int marginStartInPx = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + marginStartInDp[i], + getResources().getDisplayMetrics() + ); + + set.connect(userView.getId(), ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, marginTopInPx); + set.connect(userView.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, marginStartInPx); + + set.applyTo(layout); + + if(i < 6){ + i++; + } + } + } + + //プッシュ通知 + private void showChatNotification(String friendName) { + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + + String channelId = "chat_channel"; + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.drawable.default_icon) // 通知アイコン + .setContentTitle("新しいチャット") + .setContentText(friendName + " さんから通話があります") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true); // タップしたら消える + NotificationChannel channel = new NotificationChannel( + channelId, + "Chat Notifications", + NotificationManager.IMPORTANCE_HIGH + ); + notificationManager.createNotificationChannel(channel); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED) { + Log.d("NotificationTest", "notify() 呼ばれた!!"); + notificationManager.notify(0, builder.build()); + } else { + // 許可されていない → リクエストする + ActivityCompat.requestPermissions( + this, + new String[]{android.Manifest.permission.POST_NOTIFICATIONS}, + 1001 + ); + } + } else { + Log.d("NotificationTest", "notify() 呼ばれた!!"); + notificationManager.notify(0, builder.build()); + } + } + + //通知を受信したときのダイアログ これが動いてます + //friendName+から通話があります。ってでるから通知が来たときのフレンドを変数に置く必要がある + @Override + public void onNotificationReceived() { + // 通知を受信したときにダイアログを表示 + runOnUiThread(() -> showChatNotification("user02")); + // アイコンを赤枠に + //runOnUiThread(() -> highlightUserIcon("user01")); + } } \ No newline at end of file