diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a3f844e..341686f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,14 +15,15 @@ android:theme="@style/Theme.Tampopoclient" android:usesCleartextTraffic="true" tools:targetApi="31"> - - - - + + + @@ -46,19 +47,18 @@ - - - + + - - - - - + + + - \ No newline at end of file + 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/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..2c455f2 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,13 +4,13 @@ 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; +import com.example.tampopo_client.resources.UserResource; -import java.util.ArrayList; import java.util.List; +import java.util.Map; import retrofit2.Call; import retrofit2.Callback; @@ -24,22 +24,60 @@ * @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; + private final Map>> friendToActivitiesLiveData; // key=userId + private final MutableLiveData> friendUserIdsLiveData; // フレンドのユーザーIDのリスト + private final MutableLiveData myLatestActivityLiveData; // 自分の最新のアクティビティ - public ActivityViewModel() { - final Retrofit retrofit = new Retrofit.Builder() - .baseUrl("http://nitta-lab-www.is.konan-u.ac.jp/tampopo/") - .addConverterFactory(JacksonConverterFactory.create()) - .build(); + private final String userId; + private final String token; + + public ActivityViewModel(String userId, String token) { + this.userId = userId; + this.token = token; + + // 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()); + friendToActivitiesLiveData = Map.of(); + friendUserIdsLiveData = new MutableLiveData<>(List.of()); + myLatestActivityLiveData = new MutableLiveData<>(); + } + + @Override + public Runnable onUpdate() { + return () -> { + if (friendToActivitiesLiveData == null || friendUserIdsLiveData == null || myLatestActivityLiveData == null) { + return; + } + + // 最新のアクティビティを取得して更新する + if (friendUserIdsLiveData.isInitialized() && friendUserIdsLiveData.getValue() != null) { + for (String userId : friendUserIdsLiveData.getValue()) { + pullLatestActivity(userId, activitiesResource, friendToActivitiesLiveData); + } + } + + // 最新のフレンドのユーザーIDを取得して更新する + if (friendUserIdsLiveData.isInitialized()) { + pullLatestFriendUserIds(userId, token); + } + + // Logging + Log.d(ActivityViewModel.class.getSimpleName(), "Polling data from the server."); + }; } public void createActivity(String userId, String token, String newActivity) { + if (!myLatestActivityLiveData.isInitialized()) { + return; + } + Call createActivityCall = activitiesResource.addActivity(userId, token, newActivity); createActivityCall.enqueue(new Callback() { @Override @@ -47,18 +85,16 @@ if (response.isSuccessful()) { String createdActivityId = response.body(); - // TODO: 仮作成なので改善必須 + // TODO: 仮作成なので改善したい Call getActivityCall = activitiesResource.getActivity(userId, createdActivityId); getActivityCall.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - List activities = activitiesLiveData.getValue(); - if (activities == null) { - activities = new ArrayList<>(); - } Activity createdActivity = response.body(); - activities.add(createdActivity); - activitiesLiveData.postValue(activities); + if (createdActivity == null) { + return; + } + myLatestActivityLiveData.postValue(createdActivity); } @Override @@ -73,12 +109,101 @@ @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() { - return activitiesLiveData; + /** + * 最新のユーザーのアクティビティを取得・更新する + * + * @param userId 取得対象のユーザーのID + * @param resource アクティビティのリソース + * @param friendToActivitiesLiveData フレンドのユーザーIDからアクティビティへの写像のライブデータ + */ + private void pullLatestActivity(String userId, ActivitiesResource resource, Map>> friendToActivitiesLiveData) { + Call> fetchActivityCall = resource.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) { + return; + } + + if (friendToActivitiesLiveData.get(userId) == null) { + friendToActivitiesLiveData.put(userId, new MutableLiveData<>(List.of())); + } + MutableLiveData> userActivitiesLiveData = friendToActivitiesLiveData.get(userId); + assert userActivitiesLiveData != null; + + List userActivities = userActivitiesLiveData.getValue(); + if (userActivities == null) { + return; + } + if (fetchedActivities.isEmpty()) { + return; + } + + if (userActivities.isEmpty()) { + userActivitiesLiveData.postValue(fetchedActivities); + } else { + if (userActivities.equals(fetchedActivities)) { + return; + } + userActivitiesLiveData.postValue(fetchedActivities); + } + } + } + + @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 getMyLatestActivityLiveData() { + return myLatestActivityLiveData; + } + + public MutableLiveData> getActivitiesLiveDataFromUserId(String userId) { + return friendToActivitiesLiveData.get(userId); + } + + 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 275eb6b..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; @@ -13,6 +14,7 @@ import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.jackson.JacksonConverterFactory; +import retrofit2.converter.scalars.ScalarsConverterFactory; public class FriendReceivedRequestViewModel extends ViewModel { //サーバー(API)と通信するためのツール @@ -28,10 +30,11 @@ 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); - 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 b7a0baa..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(); @@ -51,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) { @@ -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/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/FriendIconView.java b/app/src/main/java/com/example/tampopo_client/views/FriendIconView.java new file mode 100644 index 0000000..c038a2d --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/views/FriendIconView.java @@ -0,0 +1,279 @@ +package com.example.tampopo_client.views; + +import static androidx.core.content.ContextCompat.startActivity; + +import android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import com.example.tampopo_client.R; +import com.google.android.material.imageview.ShapeableImageView; + +/** + * TODO: document your custom view class. + */ +public class FriendIconView extends FrameLayout { + private String mExampleString; // TODO: use a default from R.string... +// private int mExampleColor = Color.RED; // TODO: use a default from R.color... + private float mExampleDimension = 0; // TODO: use a default from R.dimen... + private Drawable mExampleDrawable; + + private TextPaint mTextPaint; + private float mTextWidth; + private float mTextHeight; + private ShapeableImageView mFriendIcon; + private ImageView mFriendCommentImage; + private TextView mFriendComment; + private TextView mFriendNickname; + private ImageView mFriendChatNotification; + private boolean chatNotification = false; + + public FriendIconView(Context context) { + super(context); + init(null, 0); + } + + public FriendIconView(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public FriendIconView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(attrs, defStyle); + } + + private void init(AttributeSet attrs, int defStyle) { + LayoutInflater.from(getContext()).inflate(R.layout.sample_friend_icon_view,this,true); + mFriendCommentImage = findViewById(R.id.friend_comment_image); + mFriendComment = findViewById(R.id.friend_comment); + mFriendNickname = findViewById(R.id.friend_nickname); + mFriendIcon = findViewById(R.id.friend_icon); + mFriendChatNotification = findViewById(R.id.chat_notification); + + //true(チャット通知が来た時)なら表示 + if(chatNotification){ + mFriendChatNotification.setVisibility(View.VISIBLE); + } + //false(チャット通知が来ていない)なら非表示 + else{ + mFriendChatNotification.setVisibility(View.GONE); + } + + //iconを押したらチャットを始めることができる(チャットのダイアログができたらFriendActivityを変更) + mFriendIcon.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Context ctx = getContext(); + Intent intent = new Intent(ctx, FriendActivity. class); + ctx.startActivity(intent); + } + }); +// if(attrs != null){ +// TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.FriendIconView, defStyle, 0); +// String nickname = a.getString(R.styleable.FriendIconView_nickname); +// if(nickname != null){ +// setNickname(nickname); +// } +// Drawable iconDrawable = a.getDrawable(R.styleable.FriendIconView_iconSrc); +// if(iconDrawable != null){ +// mFriendIcon.setImageDrawable(iconDrawable); +// } +// a.recycle(); +// } + + // Load attributes +// final TypedArray a = getContext().obtainStyledAttributes( +// attrs, R.styleable.FriendIconView, defStyle, 0); +// +// mExampleString = a.getString( +// R.styleable.FriendIconView_exampleString); +// mExampleColor = a.getColor( +// R.styleable.FriendIconView_exampleColor, +// mExampleColor); +// // Use getDimensionPixelSize or getDimensionPixelOffset when dealing with +// // values that should fall on pixel boundaries. +// mExampleDimension = a.getDimension( +// R.styleable.FriendIconView_exampleDimension, +// mExampleDimension); +// +// if (a.hasValue(R.styleable.FriendIconView_exampleDrawable)) { +// mExampleDrawable = a.getDrawable( +// R.styleable.FriendIconView_exampleDrawable); +// mExampleDrawable.setCallback(this); +// } + +// // Set up a default TextPaint object +// mTextPaint = new TextPaint(); +// mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG); +// mTextPaint.setTextAlign(Paint.Align.LEFT); +// +// // Update TextPaint and text measurements from attributes +// invalidateTextPaintAndMeasurements(); +// + } + + public void setNickname(String nickname) { + if (mFriendNickname != null) { + mFriendNickname.setText(nickname); + } + } + + public void setImageResource(int resId) { + if (mFriendIcon != null) { + mFriendIcon.setImageResource(resId); + } + } + + public void setComment(String comment){ + if(mFriendComment != null){ + int comment_length = comment.length(); + if (comment_length > 20) { + mFriendComment.setTextSize(7); + } else if (comment_length > 10) { + mFriendComment.setTextSize(10); + } else { + mFriendComment.setTextSize(12); + } + mFriendComment.setText(comment); + } + } + + public void setChatNotification(boolean chat){ + //true(チャット通知が来た時)なら表示 + if(chat){ + mFriendChatNotification.setVisibility(View.VISIBLE); + } + //false(チャット通知が来ていない)なら非表示 + else{ + mFriendChatNotification.setVisibility(View.GONE); + } + } +// private void invalidateTextPaintAndMeasurements() { +// mTextPaint.setTextSize(mExampleDimension); +// mTextPaint.setColor(mExampleColor); +// mTextWidth = mTextPaint.measureText(mExampleString); +// +// Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); +// mTextHeight = fontMetrics.bottom; +// } + +// @Override +// protected void onDraw(Canvas canvas) { +// super.onDraw(canvas); +// +// // TODO: consider storing these as member variables to reduce +// // allocations per draw cycle. +// int paddingLeft = getPaddingLeft(); +// int paddingTop = getPaddingTop(); +// int paddingRight = getPaddingRight(); +// int paddingBottom = getPaddingBottom(); +// +// int contentWidth = getWidth() - paddingLeft - paddingRight; +// int contentHeight = getHeight() - paddingTop - paddingBottom; +// +// // Draw the text. +// canvas.drawText(mExampleString, +// paddingLeft + (contentWidth - mTextWidth) / 2, +// paddingTop + (contentHeight + mTextHeight) / 2, +// mTextPaint); +// +// // Draw the example drawable on top of the text. +// if (mExampleDrawable != null) { +// mExampleDrawable.setBounds(paddingLeft, paddingTop, +// paddingLeft + contentWidth, paddingTop + contentHeight); +// mExampleDrawable.draw(canvas); +// } +// } + + /** + * Gets the example string attribute value. + * + * @return The example string attribute value. + */ + public String getExampleString() { + return mExampleString; + } + + /** + * Sets the view"s example string attribute value. In the example view, this string + * is the text to draw. + * + * @param exampleString The example string attribute value to use. + */ +// public void setExampleString(String exampleString) { +// mExampleString = exampleString; +// invalidateTextPaintAndMeasurements(); +// } + + /** + * Gets the example color attribute value. + * + * @return The example color attribute value. + */ +// public int getExampleColor() { +// return mExampleColor; +// } + + /** + * Sets the view"s example color attribute value. In the example view, this color + * is the font color. + * + * @param exampleColor The example color attribute value to use. + */ +// public void setExampleColor(int exampleColor) { +// mExampleColor = exampleColor; +// invalidateTextPaintAndMeasurements(); +// } + + /** + * Gets the example dimension attribute value. + * + * @return The example dimension attribute value. + */ + public float getExampleDimension() { + return mExampleDimension; + } + + /** + * Sets the view"s example dimension attribute value. In the example view, this dimension + * is the font size. + * + * @param exampleDimension The example dimension attribute value to use. + */ +// public void setExampleDimension(float exampleDimension) { +// mExampleDimension = exampleDimension; +// invalidateTextPaintAndMeasurements(); +// } + + /** + * Gets the example drawable attribute value. + * + * @return The example drawable attribute value. + */ + public Drawable getExampleDrawable() { + return mExampleDrawable; + } + + /** + * Sets the view"s example drawable attribute value. In the example view, this drawable is + * drawn above the text. + * + * @param exampleDrawable The example drawable attribute value to use. + */ + public void setExampleDrawable(Drawable exampleDrawable) { + mExampleDrawable = exampleDrawable; + } +} \ No newline at end of file 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..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 @@ -15,35 +15,41 @@ 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.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列(縦方向リスト) - /** - * Mandatory empty constructor for the fragment manager to instantiate the - * fragment (e.g. upon screen orientation changes). - */ + // アプリ全体で共有するクラス(ユーザー情報やトークンを持っている) + private Tampopo tampopo; + // RecyclerView のアダプタ(友達申請を表示する) + private MyFriendRequestRecyclerViewAdapter adapter; + + // デフォルトのコンストラクタ(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; } @@ -52,45 +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(); // 自分のユーザー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)); + // アダプタを生成してセット(最初は空のリストを渡す) + 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 4553ea6..7228d1c 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,27 @@ 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 com.example.tampopo_client.viewmodels.ActivityViewModelFactory; +import com.example.tampopo_client.viewmodels.ChatViewModel; +import com.example.tampopo_client.viewmodels.NotificationListener; -public class MainActivity extends AppCompatActivity { +import java.util.Map; + +public class MainActivity extends AppCompatActivity implements NotificationListener { private EditText editMessage; private ImageButton sendButton; private GridView wordGroup; private LinearLayout messageList; + //private List friendIds = List.of(); //アクティビティの選択肢 private String[] words = {"ひまnow", "あそぼ!", "そろそろ会いたない〜?", "勉強なう", "電話しよ~", "お風呂入ってくる~", "今暇だよー!", "いそがしい~!!"}; private Button openDialogButton; @@ -37,6 +46,7 @@ ActivityViewModel activityViewModel; Tampopo tampopo; + private ChatViewModel chatViewModel; @Override protected void onCreate(Bundle savedInstanceState) { @@ -58,17 +68,41 @@ } }); - //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>() { +// @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() { // public void onClick(View v) { -// Intent intent = new Intent(MainActivity.this,notificationActivity. class); +// Intent intent = new Intent(MainActivity.this,NotificationActivity. class); // startActivity(intent); // } // }); @@ -87,7 +121,42 @@ // ボタンを押すとダイアログ表示(トリガー用) 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() { @@ -145,4 +214,31 @@ 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); +// } +// } + } + + @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 7df07fc..400d9f1 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,105 @@ 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.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; -/** - * {@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 { - private final List mValues; + // 表示するFriendRequestItemのリスト(アダプターのデータ) + private final List items; - public MyFriendRequestRecyclerViewAdapter(List items) { - mValues = items; + private FriendViewModel friendViewModel; + private String receiverId; + private String token; + + // コンストラクタ:アダプターを初期化し、表示データのリストを受け取る + 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) { - + // 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) { - holder.mItem = mValues.get(position); - //holder.mIdView.setText(mValues.get(position).id); - holder.mContentView.setText(mValues.get(position).name); + // 現在の位置の FriendRequestItem を取得して ViewHolder に保持させる + holder.mItem = items.get(position); + // 名前(name)を TextView に表示する + holder.mContentView.setText(items.get(position).getSenderId()); + + // 「許可」ボタンを押した時の処理 + holder.allowButton.setOnClickListener(v -> { + // フレンド許可APIを叩く + String senderId = holder.mItem.getSenderId(); //許可を押して相手(senderID)のIDを取得 + + // 例:ViewModelにフレンド登録処理を依頼する + friendViewModel.createFriend(token, senderId, receiverId); + + Toast.makeText(v.getContext(), "フレンド申請を許可しました", Toast.LENGTH_SHORT).show(); + }); + } + // リスト全体のアイテム数を返す(RecyclerViewに何個表示するかを教える) @Override public int getItemCount() { - return mValues.size(); + return items.size(); } + // 各行(View)を保持するための ViewHolder クラス public class ViewHolder extends RecyclerView.ViewHolder { - //public final TextView mIdView; + // TextView:表示される友達の名前 public final TextView mContentView; - public FriendRequestItem mItem; + // 表示する1つのFriendRequestItemを保持 + public FriendRequest 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/java/com/example/tampopo_client/views/TestFriendIconActivity.java b/app/src/main/java/com/example/tampopo_client/views/TestFriendIconActivity.java new file mode 100644 index 0000000..0c1facf --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/views/TestFriendIconActivity.java @@ -0,0 +1,32 @@ +package com.example.tampopo_client.views; + +import android.os.Bundle; +import android.widget.ImageButton; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.example.tampopo_client.R; + +public class TestFriendIconActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_test_friend_icon); + FriendIconView friendIconView = findViewById(R.id.friendIconView); + friendIconView.setNickname("nitta"); + friendIconView.setImageResource(R.drawable.friend01_icon); + friendIconView.setComment(("kjrig")); +// ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { +// Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); +// v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); +// return insets; +// }); + + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_notification.png b/app/src/main/res/drawable/chat_notification.png new file mode 100644 index 0000000..47a4549 --- /dev/null +++ b/app/src/main/res/drawable/chat_notification.png Binary files differ diff --git a/app/src/main/res/drawable/circle_button.xml b/app/src/main/res/drawable/circle_button.xml new file mode 100644 index 0000000..12aed7c --- /dev/null +++ b/app/src/main/res/drawable/circle_button.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/friend02_icon.png b/app/src/main/res/drawable/friend02_icon.png new file mode 100644 index 0000000..5e82c4b --- /dev/null +++ b/app/src/main/res/drawable/friend02_icon.png Binary files differ diff --git a/app/src/main/res/drawable/friend_icon.png b/app/src/main/res/drawable/friend_icon.png new file mode 100644 index 0000000..1240e45 --- /dev/null +++ b/app/src/main/res/drawable/friend_icon.png Binary files differ diff --git a/app/src/main/res/drawable/new_png.png b/app/src/main/res/drawable/new_png.png new file mode 100644 index 0000000..1fe66f3 --- /dev/null +++ b/app/src/main/res/drawable/new_png.png Binary files differ 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" /> - + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/sample_friend_icon_view.xml b/app/src/main/res/layout/sample_friend_icon_view.xml new file mode 100644 index 0000000..7270a3e --- /dev/null +++ b/app/src/main/res/layout/sample_friend_icon_view.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..8ed710d --- /dev/null +++ b/app/src/main/res/values-night/styles.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs_friend_icon_view.xml b/app/src/main/res/values/attrs_friend_icon_view.xml new file mode 100644 index 0000000..b9f829d --- /dev/null +++ b/app/src/main/res/values/attrs_friend_icon_view.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c8524cd..e2337bb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,8 @@ #FF000000 #FFFFFFFF + #FF29B6F6 + #FF039BE5 + #FFBDBDBD + #FF757575 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2bcbac..6b0a488 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,11 +1,11 @@ - tampopo-client - ホーム画面からフレンド一覧への画面遷移ボタン - ホーム画面から通知一覧への画面遷移ボタン - ホーム画面から設定画面への画面遷移ボタン - フレンド - 申請中 - 保留中 - 友達リスト + tampopo-client + ホーム画面からフレンド一覧への画面遷移ボタン + ホーム画面から通知一覧への画面遷移ボタン + ホーム画面から設定画面への画面遷移ボタン + フレンド + 申請中 + 保留中 + 友達リスト \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..7e2e6c3 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 0afc5a0..b4bacf5 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -6,8 +6,4 @@ \ No newline at end of file