diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7bee9cc..c9e9a00 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + notificationListeners = new ArrayList<>(); - private static final double NOTIFICATION_RECEIVE_PROBABILITY = 0.01; + //private static final double NOTIFICATION_RECEIVE_PROBABILITY = 0.5; + //natty + private boolean notificationSent = false; // 一度だけ送信するためのフラグ + + +// @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()); +// } +// }; +// } + //natty @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()); + if (!notificationSent) { + // 10秒後に一回だけ通知を送る + notificationSent = true; // もう送らない + new Handler(Looper.getMainLooper()).postDelayed(() -> { + Log.d("ChatViewModel", "Received test notification (after 10s)."); + notificationListeners.forEach(NotificationListener::onNotificationReceived); + }, 10_000); // 10秒(10000ms) } }; } 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 c21e92b..e11d097 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,10 +39,14 @@ import com.example.tampopo_client.Tampopo; import com.example.tampopo_client.models.Activity; import com.example.tampopo_client.viewmodels.ActivityViewModel; +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 implements NotificationListener { @@ -42,12 +59,14 @@ //アクティビティの選択肢 private String[] words = {"ひまnow", "あそぼ!", "そろそろ会いたない〜?", "勉強なう", "電話しよ~", "お風呂入ってくる~", "今暇だよー!", "いそがしい~!!"}; private Button openDialogButton; + private Map userViews = new HashMap<>(); ActivityViewModel activityViewModel; Tampopo tampopo; private ChatViewModel chatViewModel; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -59,6 +78,21 @@ 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 + ); + } + } + +// handleIncomingIntent(getIntent()); //メイン画面からフレンド一覧画面への遷移 ImageButton friendButton = (ImageButton) findViewById(R.id.friend); friendButton.setOnClickListener(new View.OnClickListener() { @@ -90,10 +124,10 @@ // } // }); - MutableLiveData> activitiesLiveData = activityViewModel.getActivitiesLiveData(); - activitiesLiveData.observe(this, new Observer>() { + MutableLiveData> activitiesLiveData = activityViewModel.getActivitiesLiveDataFromUserId(tampopo.getUserId()); + activitiesLiveData.observe(this, new Observer>() { @Override - public void onChanged(Map activities) { + public void onChanged(List activities) { updateActivityView(activities); } }); @@ -121,6 +155,25 @@ // ボタンを押すとダイアログ表示(トリガー用) openDialogButton = findViewById(R.id.openDialogButton); openDialogButton.setOnClickListener(v -> showInputDialog()); + + // 仮データを作る(ユーザID、コメント) + List mockActivities = new ArrayList<>(); + mockActivities.add(new Activity("user01", "act01","ひま〜", "2025-09-25 10:00" )); + mockActivities.add(new Activity("user02", "act02","勉強してるよ", "2025-09-25 10:01")); + mockActivities.add(new Activity("user03", "act03","ねむい〜", "2025-09-25 10:02")); + + + // 表示を更新 + updateActivityView(mockActivities); + + // 疑似通知ボタン chat申請後ダイアログを表示するため、それの疑似的な申請コード + //まだ動くかわからない + //Button testNotificationButton = findViewById(R.id.openDialogButton); // 既存のボタンを利用 + //testNotificationButton.setOnClickListener(v -> { + // 本来は通知を受信したタイミングで呼ばれる + // showCallRequestDialog(MainActivity.this, "user01"); + //}); + } @Override @@ -214,12 +267,78 @@ 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; + } + } + + private void updateActivityView(List activities) { + //TextView comment = this.findViewById(R.id.friend01_comment); + //for (Activity ac: activities.values()) { + // comment.setText(ac.getText()); + //} + + ///natty ユーザごとにコメントの更新をする + LinearLayout messageList = findViewById(R.id.messageList); + + for (Activity entry : activities) { + String userId = entry.getUserId(); + Activity activity = entry; + + View userView = userViews.get(userId); + + if (userView == null) { + // 新しいユーザなので、アイコン+コメントを作成 + LinearLayout container = new LinearLayout(this); + container.setOrientation(LinearLayout.HORIZONTAL); + container.setPadding(16, 16, 16, 16); + + // ユーザのアイコン(固定) + ShapeableImageView iconView = new ShapeableImageView(this); + iconView.setLayoutParams(new LinearLayout.LayoutParams(100, 100)); + iconView.setScaleType(ImageView.ScaleType.CENTER_CROP); + iconView.setStrokeColor(ContextCompat.getColorStateList(this, R.color.red)); + iconView.setStrokeWidth(2f); + iconView.setShapeAppearanceModel( + iconView.getShapeAppearanceModel().toBuilder() + .setAllCornerSizes(50) // 丸く + .build() + ); + + // ユーザIDに応じてアイコンリソースを決定(仮にハードコード or マッピング) + iconView.setImageResource(getUserIconResource(userId)); // ←ここがポイント + + // コメント部分 + TextView commentView = new TextView(this); + commentView.setTextSize(16); + commentView.setPadding(16, 0, 0, 0); + commentView.setText(activity.getText()); + + // コンテナに追加 + container.addView(iconView); + container.addView(commentView); + + // Mapに登録、画面に追加 + userViews.put(userId, container); + messageList.addView(container); + + } else { + // 既に表示されている → コメントだけ更新 + TextView commentView = (TextView) ((LinearLayout) userView).getChildAt(1); + commentView.setText(activity.getText()); + } } + } // if (activities == null || friendIds == null) return; // // messageList.removeAllViews(); // 表示をリセット @@ -235,10 +354,173 @@ // messageList.addView(textView); // } // } + + //natty + //通知が来たときに赤丸をつける + //通知はまだできてないからいったん赤丸を表示させるっていうの書く + //この下のコードが赤丸がつくコード + //iconView.setStrokeColor(ContextCompat.getColorStateList(this, R.color.red)); + + //プッシュ通知 + private void showChatNotification(String friendName) { +// NotificationManager notificationManager = +// (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + 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 + ); + } +// ShapeableImageView iconView = new ShapeableImageView(this); +// iconView.setStrokeColor(ContextCompat.getColorStateList(this, R.color.black)); + + } else { + Log.d("NotificationTest", "notify() 呼ばれた!!"); + notificationManager.notify(0, builder.build()); + } + + } +// private static final int REQUEST_CODE_POST_NOTIFICATIONS = 1001; +// +// // 通知権限をチェックしてリクエストする +// private void checkAndRequestNotificationPermission() { +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { +// // Android 13以上 → POST_NOTIFICATIONS の確認 +// if (ContextCompat.checkSelfPermission( +// this, +// android.Manifest.permission.POST_NOTIFICATIONS +// ) != PackageManager.PERMISSION_GRANTED) { +// +// ActivityCompat.requestPermissions( +// this, +// new String[]{android.Manifest.permission.POST_NOTIFICATIONS}, +// REQUEST_CODE_POST_NOTIFICATIONS +// ); +// } +// } else { +// // Android 12以下 → 権限は存在しないので説明だけ +// Toast.makeText(this, +// "通知を受け取るには端末の設定から通知を有効にしてください", +// Toast.LENGTH_LONG).show(); +// } +// } +// @Override +// public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { +// super.onRequestPermissionsResult(requestCode, permissions, grantResults); +// +// if (requestCode == REQUEST_CODE_POST_NOTIFICATIONS) { +// if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { +// Toast.makeText(this, "通知が許可されました", Toast.LENGTH_SHORT).show(); +// } else { +// Toast.makeText(this, "通知が拒否されました。設定から有効にできます", Toast.LENGTH_LONG).show(); +// } +// } +// } + private void highlightUserIcon(String userId) { + runOnUiThread(() -> { + LinearLayout container = userViews.get(userId); + if (container != null && container.getChildCount() > 0) { + View v = container.getChildAt(0); + if (v instanceof ShapeableImageView) { + ShapeableImageView icon = (ShapeableImageView) v; + icon.setStrokeColor(ContextCompat.getColorStateList(this, R.color.red)); + icon.setStrokeWidth(6f); // 太めにして目立たせる + } + } + }); + } + + private void clearUserIconHighlight(String userId) { + runOnUiThread(() -> { + LinearLayout container = userViews.get(userId); + if (container != null && container.getChildCount() > 0) { + View v = container.getChildAt(0); + if (v instanceof ShapeableImageView) { + ShapeableImageView icon = (ShapeableImageView) v; + icon.setStrokeWidth(0f); // 初期状態(枠なし)に戻す + } + } + }); + } + + + + //通話が来たときのダイアログ + // 通話リクエストを受け取った時に呼び出すダイアログ + public void showCallRequestDialog(Context context, String fromUserName) { + + Dialog dialog = new Dialog(context); + dialog.setContentView(R.layout.dialog_chat_receved); + dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent); + dialog.getWindow().setDimAmount(0.5f); + + // メッセージテキスト + TextView tvMessage = dialog.findViewById(R.id.tv_message); + tvMessage.setText(fromUserName + " さんから通話リクエストが届いています。\n通話を開始しますか?"); + + // 開始ボタン + Button btnStart = dialog.findViewById(R.id.btn_start); + btnStart.setOnClickListener(v -> { + Toast.makeText(context, "通話を開始しました", Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + // TODO: 実際の通話開始処理をここに書く + }); + + // キャンセルボタン + Button btnCancel = dialog.findViewById(R.id.btn_cancel); + btnCancel.setOnClickListener(v -> dialog.dismiss()); + + dialog.show(); } @Override public void onNotificationReceived() { - // TODO: 通知を受信したときの処理を実装する + // 通知を受信したときにダイアログを表示 + runOnUiThread(() -> showChatNotification("user01")); + // アイコンを赤枠に + runOnUiThread(() -> highlightUserIcon("user01")); } +// @Override +// protected void onNewIntent(Intent intent) { +// super.onNewIntent(intent); +// setIntent(intent); // 忘れずに更新 +// handleIncomingIntent(intent); +// } + +// private void handleIncomingIntent(Intent intent) { +// if (intent != null && intent.hasExtra("incoming_user_id")) { +// String incomingUserId = intent.getStringExtra("incoming_user_id"); +// if (incomingUserId != null) { +// // userIdからユーザ名を取得する処理を入れてもいいし +// showCallRequestDialog(this, incomingUserId); +// } +// } +// } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/default_icon.jpg b/app/src/main/res/drawable/default_icon.jpg new file mode 100644 index 0000000..4f10e33 --- /dev/null +++ b/app/src/main/res/drawable/default_icon.jpg Binary files differ diff --git a/app/src/main/res/drawable/friend02_icon.jpg b/app/src/main/res/drawable/friend02_icon.jpg new file mode 100644 index 0000000..af14b47 --- /dev/null +++ b/app/src/main/res/drawable/friend02_icon.jpg Binary files differ diff --git a/app/src/main/res/drawable/friend02_icon.png b/app/src/main/res/drawable/friend02_icon.png deleted file mode 100644 index 5e82c4b..0000000 --- a/app/src/main/res/drawable/friend02_icon.png +++ /dev/null Binary files differ diff --git a/app/src/main/res/drawable/friend03_icon.jpg b/app/src/main/res/drawable/friend03_icon.jpg new file mode 100644 index 0000000..c225d81 --- /dev/null +++ b/app/src/main/res/drawable/friend03_icon.jpg Binary files differ diff --git a/app/src/main/res/drawable/friend04_icon.png b/app/src/main/res/drawable/friend04_icon.png new file mode 100644 index 0000000..5e82c4b --- /dev/null +++ b/app/src/main/res/drawable/friend04_icon.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 dac32e3..09733ae 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -127,7 +127,7 @@ android:layout_height="0dp" android:layout_weight="1" android:background="@android:color/transparent" - android:orientation="vertical" > + android:orientation="vertical"> @@ -153,8 +153,8 @@ android:id="@+id/friend01_comment" android:layout_width="93dp" android:layout_height="32dp" - android:text="dgfgh" android:gravity="center" + android:text="dgfgh" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.066" diff --git a/app/src/main/res/layout/dialog_chat_receved.xml b/app/src/main/res/layout/dialog_chat_receved.xml new file mode 100644 index 0000000..f21211f --- /dev/null +++ b/app/src/main/res/layout/dialog_chat_receved.xml @@ -0,0 +1,55 @@ + + + + + + + + + + +