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..4f8f14b 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 @@ -5,6 +5,9 @@ private String senderId; private String receiverId; + public FriendRequest(){ + //デフォルトコンストラクタを追加しました + } public FriendRequest(String senderId, String receiverId) { this.senderId = senderId; this.receiverId = receiverId; 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 3876ffd..31b884f 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 @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; import com.example.tampopo_client.models.Activity; import com.example.tampopo_client.resources.ActivitiesResource; @@ -13,10 +12,6 @@ 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; @@ -32,20 +27,20 @@ * @author Shohei Yamagiwa * @implNote Repositoryは作成せずに、すべてViewModelで処理する */ -public class ActivityViewModel extends ViewModel { +public class ActivityViewModel extends RealTimeViewModel { private final ActivitiesResource activitiesResource; private final UserResource userResource; private final MutableLiveData> activitiesLiveData; // key=userId, value=activity private final MutableLiveData> friendUserIdsLiveData; - private final ScheduledExecutorService fetchActivitiesTaskScheduler; - private ScheduledFuture fetchActivitiesTask; + private final String userId; + private final String token; - private final ScheduledExecutorService fetchFriendIdsTaskScheduler; - private ScheduledFuture fetchFriendIdsTask; + public ActivityViewModel(String userId, String token) { + this.userId = userId; + this.token = token; - public ActivityViewModel() { // Retrofitの初期化 final Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://nitta-lab-www.is.konan-u.ac.jp/tampopo/") @@ -57,68 +52,30 @@ // 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()) { + public Runnable onUpdate() { + return () -> { + if (activitiesLiveData == null || friendUserIdsLiveData == null) { return; } - if (friendUserIdsLiveData.getValue() == null) { - return; + + // 最新のアクティビティを取得して更新する + if (friendUserIdsLiveData.isInitialized() && friendUserIdsLiveData.getValue() != null) { + for (String userId : friendUserIdsLiveData.getValue()) { + pullLatestActivity(userId); + } } - for (String userId : friendUserIdsLiveData.getValue()) { - pullLatestActivity(userId); + + // 最新のフレンドのユーザーIDを取得して更新する + if (friendUserIdsLiveData.isInitialized()) { + pullLatestFriendUserIds(userId, token); } + + // Logging + Log.d(ActivityViewModel.class.getSimpleName(), "Polling data from the server."); }; - 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) { @@ -232,4 +189,12 @@ public MutableLiveData> getFriendUserIdsLiveData() { return friendUserIdsLiveData; } + + public String getUserId() { + return userId; + } + + public String getToken() { + return token; + } } 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..f52c8bf --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/ActivityViewModelFactory.java @@ -0,0 +1,36 @@ +package com.example.tampopo_client.viewmodels; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import java.lang.reflect.InvocationTargetException; + +/** + * ViewModelのライフサイクルの関係で、インスタンスを生成するためのFactoryクラスが必要 + * + * @author Shohei Yamagiwa + */ +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 + public T create(@NonNull Class modelClass) { + if (modelClass.isAssignableFrom(ActivityViewModel.class)) { + try { + return modelClass.getConstructor(String.class, String.class).newInstance(userId, token); + } catch (InvocationTargetException | IllegalAccessException | InstantiationException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + throw new IllegalStateException("作成するViewModelが異なります。"); + } +} diff --git a/app/src/main/java/com/example/tampopo_client/viewmodels/ChatViewModel.java b/app/src/main/java/com/example/tampopo_client/viewmodels/ChatViewModel.java new file mode 100644 index 0000000..3f0ca2c --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/ChatViewModel.java @@ -0,0 +1,33 @@ +package com.example.tampopo_client.viewmodels; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +public class ChatViewModel extends RealTimeViewModel { + private final List notificationListeners = new ArrayList<>(); + + private static final double NOTIFICATION_RECEIVE_PROBABILITY = 0.01; + + @Override + public Runnable onUpdate() { + return () -> { + // 1% の確率で onNotificationReceived() が呼び出される + double borderValue = Math.floor(Math.random() * 100); + double currentValue = NOTIFICATION_RECEIVE_PROBABILITY * 100; + if (currentValue >= borderValue) { + Log.d("ChatViewModel", "Received test notification."); + notificationListeners.forEach(listener -> listener.onNotificationReceived()); + } + }; + } + + public void addNotificationListener(NotificationListener listener) { + notificationListeners.add(listener); + } + + public void clearNotificationListener() { + notificationListeners.clear(); + } +} 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 2542cc1..0b01c76 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 @@ -6,6 +6,7 @@ import com.example.tampopo_client.models.FriendRequest; import com.example.tampopo_client.resources.FriendRequestsResource; +import java.util.ArrayList; import java.util.List; import retrofit2.Call; @@ -33,7 +34,7 @@ .addConverterFactory(JacksonConverterFactory.create()) .build(); this.friendRequestsResource = retrofit.create(FriendRequestsResource.class); - this.receivedRequests = new MutableLiveData<>(); + this.receivedRequests = new MutableLiveData<>(new ArrayList<>()); this.operationResult = new MutableLiveData<>(); } 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 2b56e15..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 @@ -53,7 +53,7 @@ // サーバーから送信済みフレンドリクエスト一覧を取得してキャッシュに保存 public void loadSentRequests(String token) { Call> call = friendRequestsResource.getFriendRequests(token); - +// フレンド一覧をサーバーからもらう時にエラーが起きている call.enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { diff --git a/app/src/main/java/com/example/tampopo_client/viewmodels/NotificationListener.java b/app/src/main/java/com/example/tampopo_client/viewmodels/NotificationListener.java new file mode 100644 index 0000000..772483c --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/NotificationListener.java @@ -0,0 +1,5 @@ +package com.example.tampopo_client.viewmodels; + +public interface NotificationListener { + void onNotificationReceived(); +} 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..c56ac08 --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/RealTimeViewModel.java @@ -0,0 +1,52 @@ +package com.example.tampopo_client.viewmodels; + +import androidx.lifecycle.ViewModel; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * {@code onUpdate()}が1秒毎に呼び出されます。 + * + * @author Shohei Yamagiwa + */ +public abstract class RealTimeViewModel extends ViewModel { + private final ScheduledExecutorService updateScheduler; + private ScheduledFuture updateTask; + + public RealTimeViewModel() { + updateScheduler = Executors.newSingleThreadScheduledExecutor(); + } + + public abstract Runnable onUpdate(); + + @Override + protected void onCleared() { + super.onCleared(); + + stopUpdating(); + } + + /** + * 定期的に{@code onUpdate()}を呼び出す + * + * @param delay 定期実行の間隔(秒) + */ + public void startUpdating(long delay) { + if (updateTask != null && !updateTask.isDone()) { + return; + } + updateTask = updateScheduler.scheduleWithFixedDelay(this.onUpdate(), 0, delay, TimeUnit.SECONDS); + } + + /** + * 定期実行を停止する + */ + public void stopUpdating() { + if (updateScheduler != null) { + updateScheduler.shutdownNow(); + } + } +} 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 b5a8976..20e07a1 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 @@ -22,33 +22,34 @@ import com.example.tampopo_client.viewmodels.FriendViewModel; import com.example.tampopo_client.viewmodels.FriendReceivedRequestViewModel; +import java.util.ArrayList; import java.util.List; /** - * A fragment representing a list of Items. + * 友達申請(受信リスト)を表示するための Fragment クラス */ public class FriendReceivedFragment extends Fragment { - // TODO: Customize parameter argument names + // RecyclerView を縦に並べるか、グリッドで並べるかを制御するためのカラム数 private static final String ARG_COLUMN_COUNT = "column-count"; // TODO: Customize parameters - private int mColumnCount = 1; + private int mColumnCount = 1; // デフォルトは1列(縦方向リスト) + // アプリ全体で共有するクラス(ユーザー情報やトークンを持っている) private Tampopo tampopo; + // RecyclerView のアダプタ(友達申請を表示する) + private MyFriendRequestRecyclerViewAdapter adapter; - /** - * Mandatory empty constructor for the fragment manager to instantiate the - * fragment (e.g. upon screen orientation changes). - */ + // デフォルトのコンストラクタ(Fragmentは必須) public FriendReceivedFragment() { } - // TODO: Customize parameter initialization + // この Fragment をインスタンス化するときに、カラム数を指定するためのファクトリーメソッド @SuppressWarnings("unused") public static FriendReceivedFragment newInstance(int columnCount) { FriendReceivedFragment fragment = new FriendReceivedFragment(); Bundle args = new Bundle(); - args.putInt(ARG_COLUMN_COUNT, columnCount); + args.putInt(ARG_COLUMN_COUNT, columnCount); // 引数に列数を渡す fragment.setArguments(args); return fragment; } @@ -57,53 +58,66 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // newInstance で渡された引数を取り出す if (getArguments() != null) { mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT); } } + //Fragment のレイアウトを作成する部分 @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // レイアウト XML を読み込む View view = inflater.inflate(R.layout.fragment_friend_received_list, container, false); + // ViewModel を取得(友達関連の操作をまとめた ViewModel) FriendViewModel friendViewModel = new ViewModelProvider(this).get(FriendViewModel.class); + // Application クラスからユーザー情報を取得 tampopo = (Tampopo) getActivity().getApplication(); - String receiverId = tampopo.getUserId(); - String token = tampopo.getToken(); + String receiverId = tampopo.getUserId(); // 自分のユーザーID + String token = tampopo.getToken(); // 認証トークン // Add some sample items. // for (int i = 1; i <= 30; i++) // FriendRequestContent.addItem(new FriendRequestContent.FriendRequestItem(Integer.toString(i), "ユーザ名" + i)); - // Set the adapter + // RecyclerView の初期化 if (view instanceof RecyclerView) { Context context = view.getContext(); RecyclerView recyclerView = (RecyclerView) view; + // 1列なら縦スクロールリスト、2列以上ならグリッド表示 if (mColumnCount <= 1) { recyclerView.setLayoutManager(new LinearLayoutManager(context)); } else { recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount)); } - recyclerView.setAdapter(new MyFriendRequestRecyclerViewAdapter(FriendRequestContent.ITEMS, friendViewModel, receiverId, token)); + // アダプタを生成してセット(最初は空のリストを渡す) + adapter = new MyFriendRequestRecyclerViewAdapter(new ArrayList<>(), friendViewModel, receiverId, token); + recyclerView.setAdapter(adapter); } return view; } + //View が作成されたあとに呼ばれる。ここで LiveData の監視を始める。 @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + // 受信した友達申請を管理する ViewModel を取得 FriendReceivedRequestViewModel friendReceivedRequestViewModel = new ViewModelProvider(this).get(FriendReceivedRequestViewModel.class); + // サーバーから受信した友達リクエスト一覧をロード friendReceivedRequestViewModel.loadReceivedRequests(tampopo.getToken()); + // LiveData を監視して、データが変わったら RecyclerView に反映 friendReceivedRequestViewModel.getReceivedRequestsLiveData().observe(getViewLifecycleOwner(), new Observer>() { // LiveData に変更があったとき(新しい友達リクエストのリストが届いたとき)に呼ばれるメソッド @Override public void onChanged(List friendRequests) { - for (FriendRequest f: friendRequests) { - FriendRequestContent.addItem(new FriendRequestContent.FriendRequestItem(f.getSenderId(), "ユーザー名")); + if(adapter != null) { + adapter.setItems(friendRequests); + adapter.notifyDataSetChanged(); } } }); 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 8c9c602..458c26e 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 @@ -37,12 +37,15 @@ import com.example.tampopo_client.viewmodels.ActivityViewModel; 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.HashMap; import java.util.List; import java.util.Map; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements NotificationListener { private EditText editMessage; private ImageButton sendButton; @@ -57,6 +60,7 @@ ActivityViewModel activityViewModel; Tampopo tampopo; + private ChatViewModel chatViewModel; @Override protected void onCreate(Bundle savedInstanceState) { @@ -78,11 +82,16 @@ } }); - //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); + + // ChatViewModelを初期化する + chatViewModel = new ViewModelProvider(this).get(ChatViewModel.class); + // MutableLiveData>friendUserIdsLiveDate = activityViewModel.getFriendUserIdsLiveData(); // friendUserIdsLiveDate.observe(this, new Observer>() { @@ -147,6 +156,42 @@ } + @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() { // カスタムビュー読み込み View dialogView = getLayoutInflater().inflate(R.layout.main_dialog, null); @@ -348,4 +393,8 @@ dialog.show(); } + @Override + public void onNotificationReceived() { + // TODO: 通知を受信したときの処理を実装する + } } \ 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 4b7c836..eeff46e 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 @@ -7,12 +7,14 @@ import android.view.ViewGroup; import android.widget.TextView; +import com.example.tampopo_client.models.FriendRequest; 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.ArrayList; import java.util.List; @@ -20,21 +22,26 @@ public class MyFriendRequestRecyclerViewAdapter extends RecyclerView.Adapter { // 表示するFriendRequestItemのリスト(アダプターのデータ) - private final List mValues; + private final List items; private FriendViewModel friendViewModel; private String receiverId; private String token; // コンストラクタ:アダプターを初期化し、表示データのリストを受け取る - public MyFriendRequestRecyclerViewAdapter(List items, FriendViewModel friendViewModel, String receiverId, String token) { - mValues = items; + public MyFriendRequestRecyclerViewAdapter(List items, FriendViewModel friendViewModel, String receiverId, String token) { + this.items = new ArrayList<>(items); this.friendViewModel = friendViewModel; this.receiverId = receiverId; this.token = token; } + public void setItems(List newItems) { + items.clear(); + items.addAll(newItems); + } + // ビュー(行)を新しく作成するときに呼ばれる(レイアウトのXMLを元に1行分のViewを作成) @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { @@ -47,14 +54,14 @@ @Override public void onBindViewHolder(final ViewHolder holder, int position) { // 現在の位置の FriendRequestItem を取得して ViewHolder に保持させる - holder.mItem = mValues.get(position); + holder.mItem = items.get(position); // 名前(name)を TextView に表示する - holder.mContentView.setText(mValues.get(position).name); + holder.mContentView.setText(items.get(position).getSenderId()); // 「許可」ボタンを押した時の処理 holder.allowButton.setOnClickListener(v -> { // フレンド許可APIを叩く - String senderId = holder.mItem.id; // または holder.mItem.senderId + String senderId = holder.mItem.getId().toString(); // または holder.mItem.senderId // 例:ViewModelにフレンド登録処理を依頼する friendViewModel.createFriend(token, senderId, receiverId); @@ -67,7 +74,7 @@ // リスト全体のアイテム数を返す(RecyclerViewに何個表示するかを教える) @Override public int getItemCount() { - return mValues.size(); + return items.size(); } // 各行(View)を保持するための ViewHolder クラス @@ -75,7 +82,7 @@ // TextView:表示される友達の名前 public final TextView mContentView; // 表示する1つのFriendRequestItemを保持 - public FriendRequestItem mItem; + public FriendRequest mItem; public View allowButton; // ViewHolderのコンストラクタ:バインディングされたViewを使ってUI部品にアクセスする