15
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「提出期限が近づいても危機感が持てない人」のためのMoodleアプリを作った時のお話

Last updated at Posted at 2025-01-03

1. はじめに

初めまして、Punyoです。

去年末、筆者が所属する名工大プログラミング部C0deにて、ピクシブ株式会社様の協賛とOBの先輩方のご協力のもと「第2回 C0deハッカソン with pixiv」が開催されました。このハッカソンへの多大なるご支援に対して、この場を借りて深く感謝申し上げます。

本記事は、筆者が関わったアプリの制作に至るまでのきっかけ、機能紹介、技術的な詳細やハッカソン全体を通した反省点に関しての記事となります。

2. 成果物と制作のきっかけ

今回のハッカソンでの成果物は以下の投稿の通りです。

筆者はこのアプリで以下の部分を担当しました。

  • データレイヤすべて
  • 設定画面のUI(3-(b)参照

このアプリを制作するに至ったきっかけの一つとして、筆者がMoodle公式アプリの機能において不満を持っていたということが挙げられます。具体的には

  • 今出されている課題を一目で確認できないため課題を忘れやすい
  • 提出済みの課題を一目で確認できない

などです。課題に関する情報を公式アプリで確認するためには以下のようなコース一覧から教科をタップした後、その教科の全課題のリストから該当する課題を選択する必要があります。

Screenshot_20250101_232904.png

また、筆者の高校の同期であり、筆者とは別の大学に通っている Aくん との以下のようなやり取りもきっかけとして挙げられます。なお、一部原文より文章を改変しています。

script2.png
script1.png
script3.png
script4.png

彼こそがタイトルに登場する 「提出期限が近づいても危機感が持てない人」 なのです。Aくん、ネタにしちゃってごめんね...1

これらの問題を解決するため、以下の機能を盛り込んだMoodleアプリの制作を決定しました。

  • 未提出/提出済みの課題とそれに関する情報を一目で確認できる
  • 提出期限が近づいても課題を提出していない場合指定されたアプリの使用制限を課す
  • 期限までに課題を提出できなかった場合その旨をDiscordサーバーにて晒す

3. 機能紹介

(a) 課題を管理する機能

大学のMoodleサーバーから情報を取得し、未提出/提出済みの課題とその期限をすぐに確認することができる機能を実装しました。
課題をタップすることで詳細が記されたBottomSheetを表示することができます。

Screen_recording_20250101_084510-ezgif.com-video-to-gif-converter (1).gif

(b) 期限が近づくとアプリをロックする機能

ユーザーが指定した時間までに課題を提出していなかった場合、指定されたアプリをロックする機能を実装しました。

Screen_recording_20250101_090052-ezgif.com-video-to-gif-converter.gif!

ロックされているときに該当のアプリを起動しようとすると左の画像のようにこのアプリに強制的に遷移します。なお、課題提出後は右の画像のようにメッセージが表示された後ロック解除され遷移されなくなります。

Screen_recording_20250101_094753-ezgif.com-video-to-gif-converter.gifScreen_recording_20250101_101920-ezgif.com-video-to-gif-converter (1).gif

(c) 期限までに課題を提出できなかった場合Discordサーバーにメッセージを送信する機能(未実装)

ユーザーが提出期限までに課題を提出できなかった場合、事前に指定したDiscordサーバーのチャンネルにメッセージを送信する機能を実装しようとしましたが、Discordのアカウントでのログイン(左)とサーバー・チャンネル選択(右)まで実装したところで時間切れになってしまいました。

Screenshot_20250101_105003.pngScreenshot_20250101_104243.png

4. 技術的な詳細

(a) システム全体の構成

今回作成したアプリのシステム構成は以下の通りです。

名称未設定ファイル.png

Ktorを用いて大学のMoodleサーバーや、Azure App Service上にNode.jsで構築されたバックエンドと通信しています。さらにそのバックエンドからDiscordのAPIを叩きBotの制御を行ったり、ユーザーが所属するサーバーのリストを取得しています。

(b) Moodleからのデータ取得

Moodleアプリの開発者向けドキュメントでは公式Moodleアプリが叩いているAPI等に関する詳しい情報が得られなかったため、今回はプロキシサーバーのFiddler Classicを利用して公式アプリの通信解析を行い必要な情報を取得するためのエンドポイント等を調べました。

解析によって得られた情報とMoodleアプリのソースコードを元にMoodleサーバーとの通信をKotlinで再実装しました。(実装したコードは↓から)

(c) アプリの使用制限

アプリの起動制限機能については以下のような処理の流れになっています。

(i) アクセシビリティサービスを有効化させる

アプリ起動時にこのアプリがアクセシビリティサービスを利用可能かのチェックを行い、不可能であった場合はユーザーに許可を促すダイアログを表示します。

この機能は後述する画面状態の変更の監視に利用します。

(ii) 一定時間間隔でサーバーから課題データを取得する処理を開始する

設定画面(3-(c)参照)から制限開始時間が設定されると、以下の処理が実行されサーバーから一定時間間隔で課題データを取得するバックグラウンド処理AppLimitAlarmSetterWorkerが開始されます。

suspend fun setAppLimitHour(context: Context, hour: Int) {
    context.dataStore.edit { preferences -> preferences[appLimitHourPreference] = hour }
    val workManager = WorkManager.getInstance(context)
    workManager.cancelAllWorkByTag(AppLimitAlarmSetterWorker.WORKER_TAG)
    workManager.enqueue(AppLimitAlarmSetterWorker.createWorkRequest())
}

前述したAppLimitAlarmSetterWorkerにおいて新しい課題データを取得した場合、以下の処理によりAlarmManager.setExactAndAllowWhileIdle()を用いて(iii)を課題提出時間のlimitHour時間前に実行するようにセットします。また、この処理においてIntent.putExtra()を利用して該当課題のIDを渡しています。IDは(iii)にて提出状況の確認に利用します。

val intent = Intent(applicationContext, AppLimitBroadcastReceiver::class.java)
intent.putExtra(INTENT_EXTRA_APP_LIMIT_BROADCAST_RECEIVER_ASSIGNMENT_ID, assignment.id)
val pendingIntent =
    PendingIntent.getBroadcast(
        applicationContext,
        assignment.id,
        intent,
        PendingIntent.FLAG_IMMUTABLE
    )
alarmManager.setExactAndAllowWhileIdle(
    AlarmManager.RTC_WAKEUP,
    (assignment.duedate - (limitHour * 60 * 60)) * 1000,
    pendingIntent
)

(iii) 課題提出時間のlimitHour時間前に課題の提出状況の確認を行う

課題提出時間のlimitHour時間前に課題の提出状況の確認を行い、もし課題が提出されていなかった場合はアプリのロックの有無を管理するフラグをtrueに設定します。

(iv) 画面状態の変更を監視しているアクセシビリティサービスから画面の遷移を行う

以下のような処理により、ロックの有無を管理するフラグがtrueかつ、画面状態の変更によりロック対象のアプリが表示された時にこのアプリのMainActivityを起動してロック画面への遷移を行います。

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    if (
        event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
            ::limitedAppPackageNamesList.isInitialized
    ) {
        val packageName = event.packageName.toString()
        if (limitedAppPackageNamesList.contains(packageName) && isAppLimitEnabled) {
            val result =
                CoroutineScope(Dispatchers.IO).async {
                    val appLimitRepository = AppLimitRepository(this@AppLimitService)
                    appLimitRepository.getAppLimitCauseAssignmentId()
                }
            val intent = Intent(baseContext, MainActivity::class.java)
            intent.putExtra(INTENT_EXTRA_OPEN_APP_LIMIT_SCREEN, true)
            intent.putExtra(INTENT_EXTRA_OPEN_APP_LIMIT_SCREEN_ASSIGNMENT_ID, result.getCompleted())
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            startActivity(intent)
        }
    }
}

5. 反省点

最初に「この機能をこの日までに実装する」等細かい期限を決めずに開発を進めてしまったため、最終日やその一日前に徹夜して開発する羽目になり、一部機能が未完成のまま終わってしまいました。

さらに、筆者が、メンバーに割り振る仕事の量を見誤り工程が大幅に遅れてしまったことがありました。とは言っても、3日目に頼んだ課題確認画面のUI設計が最終日になっても終わっていないと聞いたときはさすがに困惑せざるを得ませんでしたが...

筆者は、今まで大まかな期限のみを定めた個人での開発を中心に行っていたため、ハッカソンのような短い期限で一つのものを形にすることに対して慣れていなかったということが無計画さの原因であると考えました。

6. おわりに

今回が"実質"2初参加かつ初めてチームのまとめ役をしたハッカソンでしたが、Discordアカウントでの認証やユーザー補助サービスの利用など、これまで実装したことのなかった機能に関しての経験を得ることができました。また、仕事の割り振りやプランニングでの教訓など、個人での開発だけでは得られない様々な学びを得ることができました。

ここでの経験や反省点を活かしつつ、これからも積極的に個人や共同での開発に邁進してまいります。

最後までお読みいただきありがとうございました!

  1. 彼から前もってこの記事へのやりとりの転載許可を頂いています。念のため。

  2. 一昨年も同じハッカソンに参加させていただきましたが、初日に開発用のPCを不注意で破壊してしまってほとんど貢献できませんでした...(すみませんでした)

15
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?