diff --git a/app/src/main/java/com/example/tampopo_client/resources/FriendRequestsResource.java b/app/src/main/java/com/example/tampopo_client/resources/FriendRequestsResource.java index d119b3d..b82d1ae 100644 --- a/app/src/main/java/com/example/tampopo_client/resources/FriendRequestsResource.java +++ b/app/src/main/java/com/example/tampopo_client/resources/FriendRequestsResource.java @@ -19,7 +19,7 @@ //このAPIではリクエストのボディをx-www-form-urlencoded という形式で送ります @FormUrlEncoded @POST("friend-requests") - Call postFriendRequest( + Call postFriendRequest( @Field("sender-id") String senderId, @Field("receiver-id") String receiverId, @Field("token") String token 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 5a2e999..3876ffd 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 @@ -8,9 +8,15 @@ import com.example.tampopo_client.models.Activity; import com.example.tampopo_client.resources.ActivitiesResource; +import com.example.tampopo_client.resources.UserResource; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import retrofit2.Call; import retrofit2.Callback; @@ -18,6 +24,8 @@ import retrofit2.Retrofit; import retrofit2.converter.jackson.JacksonConverterFactory; +// NOTE: MainActivityでLiveDataをobserveし表示する + /** * アクティビティを扱うためのViewModel * @@ -26,17 +34,91 @@ */ public class ActivityViewModel extends ViewModel { private final ActivitiesResource activitiesResource; + private final UserResource userResource; - private final MutableLiveData> activitiesLiveData; + private final MutableLiveData> activitiesLiveData; // key=userId, value=activity + private final MutableLiveData> friendUserIdsLiveData; + + private final ScheduledExecutorService fetchActivitiesTaskScheduler; + private ScheduledFuture fetchActivitiesTask; + + private final ScheduledExecutorService fetchFriendIdsTaskScheduler; + private ScheduledFuture fetchFriendIdsTask; public ActivityViewModel() { + // Retrofitの初期化 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); - activitiesLiveData = new MutableLiveData<>(List.of()); + // LiveDataの初期化 + activitiesLiveData = new MutableLiveData<>(Map.of()); + friendUserIdsLiveData = new MutableLiveData<>(List.of()); + + // スケジューラーの作成 + fetchActivitiesTaskScheduler = Executors.newSingleThreadScheduledExecutor(); + fetchFriendIdsTaskScheduler = Executors.newSingleThreadScheduledExecutor(); + } + + @Override + protected void onCleared() { + super.onCleared(); + + // ViewModelの破棄時にタスクを即停止する + if (fetchActivitiesTaskScheduler != null) { + fetchActivitiesTaskScheduler.shutdownNow(); + } + if (fetchFriendIdsTaskScheduler != null) { + fetchFriendIdsTaskScheduler.shutdownNow(); + } + } + + /** + * 定期的に最新のアクティビティ一覧を取得・更新する + */ + public void startFetchingLatestActivities() { + if (fetchActivitiesTask != null && !fetchActivitiesTask.isDone()) { + return; + } + + final Runnable task = () -> { + if (!friendUserIdsLiveData.isInitialized()) { + return; + } + if (friendUserIdsLiveData.getValue() == null) { + return; + } + for (String userId : friendUserIdsLiveData.getValue()) { + pullLatestActivity(userId); + } + }; + fetchActivitiesTask = fetchActivitiesTaskScheduler.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS); + } + + /** + * 定期的に最新のフレンド一覧を取得・更新する + * + * @param userId ユーザーID + * @param token ユーザーの認証用トークン + */ + public void startFetchingLatestFriends(String userId, String token) { + if (fetchFriendIdsTask != null && !fetchFriendIdsTask.isDone()) { + return; + } + + final Runnable task = () -> { + if (!friendUserIdsLiveData.isInitialized()) { + return; + } + + pullLatestFriendUserIds(userId, token); + + Log.d(ActivityViewModel.class.getSimpleName(), "Polling friends data from the server."); + }; + fetchFriendIdsTask = fetchFriendIdsTaskScheduler.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS); } public void createActivity(String userId, String token, String newActivity) { @@ -52,12 +134,18 @@ getActivityCall.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - List activities = activitiesLiveData.getValue(); + Map activities = activitiesLiveData.getValue(); if (activities == null) { - activities = new ArrayList<>(); + activities = new HashMap<>(); } + Activity createdActivity = response.body(); - activities.add(createdActivity); + if (activities.containsKey(userId)) { + activities.replace(userId, createdActivity); + } else { + activities.put(userId, createdActivity); + } + activitiesLiveData.postValue(activities); } @@ -73,12 +161,75 @@ @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - Log.e(ActivityViewModel.class.getSimpleName(), "An error has occurred.", t); + Log.e(ActivityViewModel.class.getSimpleName(), "An error has occurred while creating new activity.", t); } }); } - public MutableLiveData> getActivitiesLiveData() { + /** + * 最新のユーザーのアクティビティを取得・更新する + * + * @param userId 取得対象のユーザーのID + */ + private void pullLatestActivity(String userId) { + Call> fetchActivityCall = activitiesResource.getActivities(userId, "LATEST"); + fetchActivityCall.enqueue(new Callback>() { + @Override + public void onResponse(@NonNull Call> call, @NonNull Response> response) { + if (response.isSuccessful()) { + List fetchedActivities = response.body(); + if (fetchedActivities != null) { + Activity latestActivity = response.body().get(0); + Map activities = activitiesLiveData.getValue(); + if (activities == null) { + activities = new HashMap<>(); + } + if (activities.containsKey(userId)) { + activities.replace(userId, latestActivity); + } else { + activities.put(userId, latestActivity); + } + + activitiesLiveData.postValue(activities); + } + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + Log.e(ActivityViewModel.class.getSimpleName(), "An error has occurred while fetching the latest activity.", t); + } + }); + } + + /** + * 最新の自分のフレンドのユーザーIDをすべて取得・更新する。 + * + * @param userId 自分のユーザーID + * @param token 自分のユーザー認証用トークン + */ + private void pullLatestFriendUserIds(String userId, String token) { + Call> fetchFriendUserIdsCall = userResource.getFriends(userId, token); + fetchFriendUserIdsCall.enqueue(new Callback>() { + @Override + public void onResponse(@NonNull Call> call, @NonNull Response> response) { + if (response.isSuccessful()) { + friendUserIdsLiveData.postValue(response.body()); + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + Log.e(ActivityViewModel.class.getSimpleName(), "An error has occurred while fetching all friends' id.", t); + } + }); + } + + public MutableLiveData> getActivitiesLiveData() { return activitiesLiveData; } + + public MutableLiveData> getFriendUserIdsLiveData() { + return friendUserIdsLiveData; + } } diff --git a/app/src/main/java/com/example/tampopo_client/viewmodels/FriendReceivedRequestViewModel.java b/app/src/main/java/com/example/tampopo_client/viewmodels/FriendReceivedRequestViewModel.java index 275eb6b..2542cc1 100644 --- a/app/src/main/java/com/example/tampopo_client/viewmodels/FriendReceivedRequestViewModel.java +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/FriendReceivedRequestViewModel.java @@ -13,6 +13,7 @@ import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.jackson.JacksonConverterFactory; +import retrofit2.converter.scalars.ScalarsConverterFactory; public class FriendReceivedRequestViewModel extends ViewModel { //サーバー(API)と通信するためのツール @@ -28,6 +29,7 @@ this.retrofit = new Retrofit.Builder() .baseUrl("http://nitta-lab-www.is.konan-u.ac.jp/tampopo/") + .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(JacksonConverterFactory.create()) .build(); this.friendRequestsResource = retrofit.create(FriendRequestsResource.class); diff --git a/app/src/main/java/com/example/tampopo_client/viewmodels/FriendSentRequestViewModel.java b/app/src/main/java/com/example/tampopo_client/viewmodels/FriendSentRequestViewModel.java index 6b15a90..3cc0a89 100644 --- a/app/src/main/java/com/example/tampopo_client/viewmodels/FriendSentRequestViewModel.java +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/FriendSentRequestViewModel.java @@ -13,6 +13,7 @@ import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.jackson.JacksonConverterFactory; +import retrofit2.converter.scalars.ScalarsConverterFactory; public class FriendSentRequestViewModel extends ViewModel { // サーバー(API)と通信するためのツール @@ -31,6 +32,7 @@ public FriendSentRequestViewModel() { this.retrofit = new Retrofit.Builder() .baseUrl("http://nitta-lab-www.is.konan-u.ac.jp/tampopo/") + .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(JacksonConverterFactory.create()) .build(); @@ -75,11 +77,11 @@ // サーバーにフレンド申請を送信するメソッド public void sendFriendRequest(String senderId, String receiverId, String token) { - Call call = friendRequestsResource.postFriendRequest(token, senderId, receiverId); + Call call = friendRequestsResource.postFriendRequest(senderId, receiverId, token); - call.enqueue(new Callback() { + call.enqueue(new Callback() { @Override - public void onResponse(Call call, Response response) { + public void onResponse(Call call, Response response) { if (response.isSuccessful()) { //Retrofitでサーバーにリクエストを送った後、その結果が帰ってくる。成功 operationResult.setValue("Friend request sent successfully."); @@ -95,7 +97,7 @@ } @Override - public void onFailure(Call call, Throwable t) { + public void onFailure(Call call, Throwable t) { operationResult.setValue("Network error: " + t.getMessage()); System.out.println("Network error: " + t); } diff --git a/app/src/main/java/com/example/tampopo_client/views/FriendReceivedFragment.java b/app/src/main/java/com/example/tampopo_client/views/FriendReceivedFragment.java index 72f9525..b5a8976 100644 --- a/app/src/main/java/com/example/tampopo_client/views/FriendReceivedFragment.java +++ b/app/src/main/java/com/example/tampopo_client/views/FriendReceivedFragment.java @@ -15,8 +15,11 @@ import android.view.ViewGroup; import com.example.tampopo_client.R; +import com.example.tampopo_client.Tampopo; import com.example.tampopo_client.models.FriendRequest; +import com.example.tampopo_client.viewmodels.FriendSentRequestViewModel; import com.example.tampopo_client.views.placeholder.FriendRequestContent; +import com.example.tampopo_client.viewmodels.FriendViewModel; import com.example.tampopo_client.viewmodels.FriendReceivedRequestViewModel; import java.util.List; @@ -31,6 +34,8 @@ // TODO: Customize parameters private int mColumnCount = 1; + private Tampopo tampopo; + /** * Mandatory empty constructor for the fragment manager to instantiate the * fragment (e.g. upon screen orientation changes). @@ -62,6 +67,13 @@ Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_friend_received_list, container, false); + FriendViewModel friendViewModel = new ViewModelProvider(this).get(FriendViewModel.class); + + tampopo = (Tampopo) getActivity().getApplication(); + String receiverId = tampopo.getUserId(); + String token = tampopo.getToken(); + + // Add some sample items. // for (int i = 1; i <= 30; i++) // FriendRequestContent.addItem(new FriendRequestContent.FriendRequestItem(Integer.toString(i), "ユーザ名" + i)); @@ -75,7 +87,7 @@ } else { recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount)); } - recyclerView.setAdapter(new MyFriendRequestRecyclerViewAdapter(FriendRequestContent.ITEMS)); + recyclerView.setAdapter(new MyFriendRequestRecyclerViewAdapter(FriendRequestContent.ITEMS, friendViewModel, receiverId, token)); } return view; } @@ -84,6 +96,7 @@ public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); FriendReceivedRequestViewModel friendReceivedRequestViewModel = new ViewModelProvider(this).get(FriendReceivedRequestViewModel.class); + friendReceivedRequestViewModel.loadReceivedRequests(tampopo.getToken()); friendReceivedRequestViewModel.getReceivedRequestsLiveData().observe(getViewLifecycleOwner(), new Observer>() { // LiveData に変更があったとき(新しい友達リクエストのリストが届いたとき)に呼ばれるメソッド 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 5fb81c2..14bf304 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 @@ -10,6 +10,7 @@ import android.widget.GridView; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.TextView; import androidx.activity.EdgeToEdge; import androidx.appcompat.app.AlertDialog; @@ -17,19 +18,26 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import com.example.tampopo_client.R; 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.UserViewModel; +import java.util.List; +import java.util.Map; + public class MainActivity extends AppCompatActivity { private EditText editMessage; private ImageButton sendButton; private GridView wordGroup; private LinearLayout messageList; + //private List friendIds = List.of(); //アクティビティの選択肢 private String[] words = {"ひまnow", "あそぼ!", "そろそろ会いたない〜?", "勉強なう", "電話しよ~", "お風呂入ってくる~", "今暇だよー!", "いそがしい~!!"}; private Button openDialogButton; @@ -64,6 +72,25 @@ tampopo = (Tampopo) getApplication(); +// MutableLiveData>friendUserIdsLiveDate = activityViewModel.getFriendUserIdsLiveData(); +// friendUserIdsLiveDate.observe(this, new Observer>() { +// @Override +// public void onChanged(List friendLive) { +// if(friendLive != null){ +// friendIds = friendLive; +// updateActivityView(activityViewModel.getActivitiesLiveData().getValue()); +// } +// } +// }); + + MutableLiveData> activitiesLiveData = activityViewModel.getActivitiesLiveData(); + activitiesLiveData.observe(this, new Observer>() { + @Override + public void onChanged(Map activities) { + updateActivityView(activities); + } + }); + //メイン画面から通知一覧画面への遷移 // ImageButton notificationButton = (ImageButton)findViewById(R.id.notification); // notificationButton.setOnClickListener(new View.OnClickListener() { @@ -87,7 +114,6 @@ // ボタンを押すとダイアログ表示(トリガー用) openDialogButton = findViewById(R.id.openDialogButton); openDialogButton.setOnClickListener(v -> showInputDialog()); - } private void showInputDialog() { @@ -145,4 +171,27 @@ dialog.show(); } + private void updateActivityView(Map activities) { + TextView comment = this.findViewById(R.id.friend01_comment); + for (Activity ac: activities.values()) { + comment.setText(ac.getText()); + } + +// 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); +// } +// } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/tampopo_client/views/MyFriendRequestRecyclerViewAdapter.java b/app/src/main/java/com/example/tampopo_client/views/MyFriendRequestRecyclerViewAdapter.java index 7df07fc..4b7c836 100644 --- a/app/src/main/java/com/example/tampopo_client/views/MyFriendRequestRecyclerViewAdapter.java +++ b/app/src/main/java/com/example/tampopo_client/views/MyFriendRequestRecyclerViewAdapter.java @@ -3,59 +3,98 @@ import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.example.tampopo_client.viewmodels.FriendReceivedRequestViewModel; +import com.example.tampopo_client.viewmodels.FriendViewModel; import com.example.tampopo_client.views.placeholder.FriendRequestContent.FriendRequestItem; import com.example.tampopo_client.databinding.FragmentFriendReceivedBinding; +import android.widget.Toast; import java.util.List; -/** - * {@link RecyclerView.Adapter} that can display a {@link FriendRequestItem}. - * TODO: Replace the implementation with code for your data type. - */ + +// このクラスは RecyclerView.Adapter を継承していて、FriendRequestItem を表示するアダプター public class MyFriendRequestRecyclerViewAdapter extends RecyclerView.Adapter { + // 表示するFriendRequestItemのリスト(アダプターのデータ) private final List mValues; - public MyFriendRequestRecyclerViewAdapter(List items) { + private FriendViewModel friendViewModel; + private String receiverId; + private String token; + + // コンストラクタ:アダプターを初期化し、表示データのリストを受け取る + public MyFriendRequestRecyclerViewAdapter(List items, FriendViewModel friendViewModel, String receiverId, String token) { mValues = items; + this.friendViewModel = friendViewModel; + this.receiverId = receiverId; + this.token = token; + } + // ビュー(行)を新しく作成するときに呼ばれる(レイアウトのXMLを元に1行分のViewを作成) @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - + // fragment_friend_received.xml を元に View を生成して ViewHolder に渡す return new ViewHolder(FragmentFriendReceivedBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); - } + // 表示するデータを、対応するViewHolderのUI部品にセットする(スクロール時など) + // onBindViewHolder:表示するアイテムの「中身」を設定する処理を書くところ @Override public void onBindViewHolder(final ViewHolder holder, int position) { + // 現在の位置の FriendRequestItem を取得して ViewHolder に保持させる holder.mItem = mValues.get(position); - //holder.mIdView.setText(mValues.get(position).id); + // 名前(name)を TextView に表示する holder.mContentView.setText(mValues.get(position).name); + + // 「許可」ボタンを押した時の処理 + holder.allowButton.setOnClickListener(v -> { + // フレンド許可APIを叩く + String senderId = holder.mItem.id; // または holder.mItem.senderId + + // 例:ViewModelにフレンド登録処理を依頼する + friendViewModel.createFriend(token, senderId, receiverId); + + Toast.makeText(v.getContext(), "フレンド申請を許可しました", Toast.LENGTH_SHORT).show(); + }); + } + // リスト全体のアイテム数を返す(RecyclerViewに何個表示するかを教える) @Override public int getItemCount() { return mValues.size(); } + // 各行(View)を保持するための ViewHolder クラス public class ViewHolder extends RecyclerView.ViewHolder { - //public final TextView mIdView; + // TextView:表示される友達の名前 public final TextView mContentView; + // 表示する1つのFriendRequestItemを保持 public FriendRequestItem mItem; + public View allowButton; + // ViewHolderのコンストラクタ:バインディングされたViewを使ってUI部品にアクセスする public ViewHolder(FragmentFriendReceivedBinding binding) { + // 親クラスのコンストラクタに、View全体を渡す super(binding.getRoot()); - //mIdView = binding.itemNumber; + // fragment_friend_received.xml 内の TextView(contentというid)に対応する変数をセット mContentView = binding.content; + allowButton = binding.AllowButton; + } + // デバッグやログ出力用に、表示中のテキストを返す @Override public String toString() { return super.toString() + " '" + mContentView.getText() + "'"; } + + public void onAccept(String id) { + } } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 31dc501..dac32e3 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -148,19 +148,6 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.366" /> -