アニメの通知アプリの制作詳細

AnimeCheckerの紹介

AnimeCheckerはアニメの放送時間をリマインドしてくれるアプリです。 アニメを視聴する時は、放送開始時間前にテレビをつけ、Twitterを開き、アニメを楽しみます。 既にアニメの放送時間を通知するアプリはありますが、多くは以下の問題があります。

  • 通知の時間が固定されていて指定できない。
    • 人によって、通知されてからテレビをつけるまでの時間は違います。
  • 通知は1度のみ
    • リマインダーとして使用されるなら、基本的に複数回通知べきです。
    • アラームアプリのようなスヌーズ機能を備えるべきです。

これらの問題を解決するためにAnimeCheckerを作りました。

github.com

AnimeCheckerは以下のような特徴があります。

  • 通知時間を選べる見逃し防止
    • 通知時間を選択式にしました
    • 可視性を低くしない為に最小限の選択肢に留めています
  • アニメごと複数回に分けて、通知する
    • 1つだけではなく、複数の通知を作成することができます
    • 複数回通知して、今行っている作業の時間配分を考えることができます
  • 少ないタップ操作
    • 放送時間の指定には、時計を使用します
    • 好きな通知をタップするだけで設定できます

機能の紹介

通知一覧画面

通知を設定したアニメを一覧で見ることができます。アプリを起動した後に移る画面です。 通知一覧画面から、通知の作成・編集・削除を行う画面に移動することができます。

f:id:ehu_J:20190717173701j:plain

通知時間を直感的な操作で簡単に設定

Chipをタップするだけです。また、複数通知を設定できます。

f:id:ehu_J:20190716200739j:plain

ハイライト

f:id:ehu_J:20190719160816g:plain

通知の例

f:id:ehu_J:20190719163457j:plain

2年前に作ったYoutubeでタイトルを通知する拡張機能をストア公開しました

chrome.google.com

2年前に作ったもの

f:id:ehu_J:20190327192045g:plain
再生している動画をタイトル付きで通知

参考程度にGooglePlayは設定で通知をONにすることができて、こんな感じ動作していた。

f:id:ehu_J:20190403181115p:plain
GooglePlayの通知 設定からONに出来る

メンテする理由

  • Chromeのバージョンアップにより、通知音がピコォーン♪ってなるようになったため(Chrome 70?)

正直うざかったので、切ってました。(放置) どうせメンテするなら、直したい->リファレンスを読む限りsilent:trueでできそう。

  • 公開しないの?って言われたため

就活中に、公開しないのって言われた。それだけ

できた

Githubリポジトリ

github.com

この拡張機能の仕組みについて

ページリロードを検出し、読み込みが完了したら通知を出すようにしています。サムネイルはurlからmovieIDをsubstringしてvar thumbnailUrl = "http://i.ytimg.com/vi/" + movieID + "/default.jpg";で取得しています。

chrome.tabs - Google Chrome

      chrome.notifications.create(
        getNotificationId(),
        {
          type : 'basic',
          iconUrl : thumbnailUrl,
          title : pageTitle,
          message : 'YouTitleの通知',
          silent : true,
        },
        function(){})

youTitle_extension/myscript.js at a1a672ccd209ff2db55629beb348eeea74e5ba22 · ehu-151/youTitle_extension · GitHub

タブの更新は

chrome.tabs - Google Chrome

でこんな感じに...

chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
  //ページのURLを取得
  var strUrl = String(tab.url);
  //通知の生成
  //statusがcompleteになった時に通知
  if (changeInfo.status == "complete" && isYoutubeTrack(strUrl)) {
    var thumbnailUrl = getThumbnailUrl(strUrl);
    var pageTitle= tab.title;
    //ページタイトルの余分な部分(末尾の" - Youtube")を削除する
    pageTitle=pageTitle.substr( 0, pageTitle.length-10);
    //通知開始
    creatNotification(thumbnailUrl,pageTitle);
  }
});

ちょっと疑問に感じたこと:onUpdated.addListenerは何故か5回通知される()

teratail.com

公開しました

5$払って公開

chrome.google.com

自己紹介

呼ばれ方

  • KD
  • ehuthon

SNS

  • Twtter
    • @ehuthon(趣味垢)
    • @kd_gg12e(大学垢)

ブログや記事など

フロー&成果物

(2016/04)東海大学入学

(2016/9-2016/10)新入生教育用電卓アプリ

(2016/10-2017/4)Wifi-Connection:大学Wifiに自動接続、途中で挫折

(2017/05-2017/06)youTitle_extension:サムネイル付きで動画のタイトルを通知

(2017/06)AndroidSpinnerストレス発散

(2017/09-2017/10)spla-victor-calculation←ブログ、スプラトゥーンの画像面積測定

(2017/11)RecodeVoiceinbackgroud

(2017/12-2018/01)初めてのAndroidプログラミング書籍をやる

(2018/02)amazon_wishlist合宿用

(2018/02-2018/03)Python勉強期間

(2018/03)Raspberrypi購入

(2018/03)Qiitaを始める

(2018/05)OneDriveAPI_Java

(2018/05)slack_botSlackのコマンドで画像を出力、枚数指定、キーワード指定可能

(2018/05-2018/06) animeface←Qiita

(2018/06)サークル内でanimefaceのLT(資料:スライド)

(2018/05-2018/07)ImageDrive画像を検索&取得し、OneDriveにアップロードして、Windowsのデスクトップに1分刻みで更新

(2018/06-2018/07)Blocks Wordビル・ゲイツのゲームDONKEY.BASを目指したもの

(2018/09-2018/10)漫画を人に勧めるときために漫画足し算をやってみたかった話(途中経過)←Qiita

(2018/08-2019-01)WeekCkech

(2018/10)Kotlin イン アクションを読み始める

(2019/02)AtCoderの勉強

(2019/02-?)AtcoderChecker←ブログ

(2019/03-2019/04)java言語で学ぶデザインパターン入門を読む

(2019-04)WorkManagerやLivedataのリファレンスを読んだり、実装を追う( 成果:Qiita )

(2019/05) RecycleViewの実装を追う。fade-inを実装する。(成果: Qiita )

(2019/05) あるあるLT〜スマホアプリ開発エンジニア〜 Vol.4で登壇 ( 資料:RecyclerViewのAnimation.pptx - Google ドライブ )

(2019/07) 10日間でAndroidアプリ開発 アニメの通知アプリの制作詳細 - ehu-151’s diary

資格

2017年12月1日 普通運転免許 取得

2018年5月16日 基本情報技術者試験 合格

2018年8月2日 画像処理エンジニア検定 ベーシック 合格

AtCoderのABC問題のAC一覧をAndroidアプリで表示させた話(即席アプリ)

f:id:ehu_J:20190226121450j:plain 2019年2月26日現在

動機

AtCoderをやっていて、ABCのC問題の壁にぶち当たり、他の問題を解こうとしたのですが、自分のやった問題が分からない!!ということになりました。@kenkoooo さんのGitHub - kenkoooo/AtCoderProblems: Problem manager for AtCoder usersを使えばできそうだったので、やってみることにしました。

開発環境

アーキテクチャjetpackガイドラインを参考にしました。 developer.android.com

アーキテクチャはしっかり考えたいところなのですが、即席なのであまり考えていません。 テストやったことないので、いつかやりないなぁ

package分け f:id:ehu_J:20190227130444p:plain

API & DB

  • HTTP client library:retorofit
  • Json parser:moshi
  • BDMS:Room

Submission APIを叩いています。帰ってきたレスポンスをmoshiでパースして、Repositotyに返しています。レスポンスのListはLiveDataで包んでいます。Fragment内でLivedata型を受け取り、変更があった時に、Viewの更新をしています。

APIのModel

class StaticsModel(
    @Json(name = "executionTime")
    val execution_time: String,
    val point: String,
    val result: String,
    @Json(name = "problemId")
    val problem_id: String,
    @Json(name = "userId")
    val user_id: String,
    @Json(name = "epochSecond")
    val epoch_second: String,
    @Json(name = "contestId")
    val contest_id: String,
    val id: String,
    val language: String,
    val length: String
)

Service

interface StaticsServise {
    @GET("atcoder-api/results")
    fun getProblems(@Query("user") user: String): Call<List<StaticsModel>>
}

DBのModel

@Entity
data class RoomStatics(
    val execution_time: String?,
    val point: String,
    val result: String,
    val problem_id: String,
    val user_id: String,
    val epoch_second: String,
    val contest_id: String,
    @PrimaryKey val id: String,
    val language: String,
    val length: String
)

DBのDao

@Dao
interface RoomStaticsDao{
    // get
    @Query("SELECT * FROM roomStatics ORDER BY epoch_second ASC")
    fun getAll(): List<RoomStatics>

    // insert
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(roomTask: RoomStatics)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(vararg roomTask: RoomStatics)

    // update
    @Update
    fun updateTasks(vararg roomTask:RoomStatics)

    // delete
    @Delete
    fun delete(roomTask: RoomStatics)
}

Database

@Database(entities = arrayOf(RoomStatics::class), version = 1)
@TypeConverters(DateTimeConverter::class)
abstract class AppDatabase : RoomDatabase() {

    // DAOを取得する。
    abstract fun roomTaskDao(): RoomStaticsDao
}

Repository

APIでインターネットから情報を受け取るメソッドと、DBに保存するメソッドとDBから情報を取得するメソッドを実装しています。

class StaticsRepository {
    //Retrofitインターフェース
    private var staticsServise: StaticsServise

    init {
        //okhttpのclient作成
        val interceptor = HttpLoggingInterceptor()
        interceptor.level = HttpLoggingInterceptor.Level.BODY
        val client = OkHttpClient.Builder().addInterceptor(interceptor).build()

        //クライアント生成

        var retrofit = Retrofit.Builder()
            .baseUrl("https://kenkoooo.com/atcoder/")
            .addConverterFactory(MoshiConverterFactory.create())
            .client(client)
            .build()
        staticsServise = retrofit.create(StaticsServise::class.java)
    }

    //APIにリクエストし、レスポンスをLiveDataで返す(StaticsModel)
    fun getReqListStatic(user: String): LiveData<List<StaticsModel>> {
        val data = MutableLiveData<List<StaticsModel>>()
        runBlocking {
            thread {
                data.postValue(staticsServise.getProblems(user).execute().body())
            }
        }
        return data
    }
    fun saveToDB(context: Context, list: List<StaticsModel>){
        thread {
            //save to room
            val db = Room.databaseBuilder(context, AppDatabase::class.java, "database-name").build()
            list.map {
                db.roomTaskDao().insert(
                    RoomStatics(
                        execution_time = it.execution_time,
                        point = it.point,
                        result = it.result,
                        problem_id = it.problem_id,
                        user_id = it.user_id,
                        epoch_second = it.epoch_second,
                        contest_id = it.contest_id,
                        id = it.id,
                        language = it.language,
                        length = it.length
                    )
                )
            }

        }
    }
    fun getDBData(context: Context): LiveData<List<StaticsModel>>{
        val data= MutableLiveData<List<StaticsModel>>()
        runBlocking {
            thread {
                val db = Room.databaseBuilder(context, AppDatabase::class.java, "database-name").build()
                val result = db.roomTaskDao().getAll()
                val model = result.map {
                    StaticsModel(
                        execution_time = it.execution_time ?: "0",
                        point = it.point,
                        result = it.result,
                        problem_id = it.problem_id,
                        user_id = it.user_id,
                        epoch_second = it.epoch_second,
                        contest_id = it.contest_id,
                        id = it.id,
                        language = it.language,
                        length = it.length
                    )
                }
                data.postValue(model)
            }
        }
        return data
    }
}

ViewModel

Androidの画面で必要な情報を保持しておくためのクラスとして設計しました。LiveDataの集約の集まりといってもいいでしょうか。

class StaticsAllProblemsViewModel : ViewModel() {
    var apiData: LiveData<List<StaticsModel>>? = null
    var dbData: LiveData<List<StaticsModel>>? = null

    fun getListDataByAPI(user: String): LiveData<List<StaticsModel>> {
        return apiData ?: StaticsRepository().getReqListStatic(user)
    }

    fun save(context: Context, list: List<StaticsModel>) {
        StaticsRepository().saveToDB(context, list)
    }

    fun getDBData(context: Context): LiveData<List<StaticsModel>> {
        return dbData ?: StaticsRepository().getDBData(context)
    }
}

Fragment

class StaticsAllProblemsFragment : Fragment() {
    lateinit var bindong: FragmentStaticsAllProblemsBinding
    lateinit var viewModel: StaticsAllProblemsViewModel

   ...

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        bindong = DataBindingUtil.inflate<FragmentStaticsAllProblemsBinding>(
            inflater,
            R.layout.fragment_statics_all_problems,
            container,
            false
        )
        //ViewModel初期化
        viewModel = ViewModelProviders.of(activity!!)
            .get(StaticsAllProblemsViewModel::class.java)

        //wifiにつながっているなら、APIで取得
        val wifiManager: WifiManager = context?.getSystemService(Context.WIFI_SERVICE) as WifiManager
        if (wifiManager.connectionInfo.supplicantState == SupplicantState.COMPLETED) {
            //有効なら
            setListDataByApiAndDB()
        } else {
            //無効なら
            setListDataByDB()
        }
        return bindong.root
    }

    // APIでデータをリクエスト&ローカルデータベースに保存&表示:wifi接続時推奨
    fun setListDataByApiAndDB() {

        viewModel.getListDataByAPI("ehu_151").observe(this, Observer {
            //save
            viewModel.save(context!!, it)
            setListDataByDB()
        })
    }

    // ローカルデータベースからデータを参照:wifi未接続時推奨
    fun setListDataByDB() {
        viewModel.getDBData(context!!).observe(this, Observer {
            val group = it.filter { Regex("abc").containsMatchIn(it.contest_id) }.groupBy { it.contest_id }
            val res = group.map {
                var contestName = it.key
                var a_result = ""
                var b_result = ""
                var c_result = ""
                var d_result = ""
                for (model in it.value) {
                    when (model.problem_id) {
                        contestName + "_a" -> a_result = model.result
                        contestName + "_b" -> b_result = model.result
                        contestName + "_c" -> c_result = model.result
                        contestName + "_d" -> d_result = model.result
                    }
                }
                StaticsAllProblemEntity(contestName, a_result, b_result, c_result, d_result)
            }
            // ここでbindingやadapterに代入
            bindong.staticsListView.adapter =
                StaticsAllProblemsAdapter(context!!, res.sortedByDescending { it.problemName })
        })
    }
}

直したいところと反省点

DBとAPIのModelがほとんど一緒

個人的な意見ですが、ほとんど同じならば、インターフェースを作成したくなりますね。しかし、自分には上手い設計方法が分からなかったので、雑な作りになっています。

Fragmentがロジックを持ってしまっている

DBから受け取ったデータをViewに表示する用のEntityに変換するために、Fragmentで変換を行う形になってしまいました。MVPアーキテクチャをやったことがある自分としては、あまりよく感じていません。本当ならば、FragmentはViewにデータを渡すだけにしたいですね。その為に、UseCaseに変換ロジック負わせて、MVVM+CleanArchitectureの形にしてもいいかもしれませんね。

以上です。良い設計に改良出来たら、またブログを書こうと思います

ホワイトボードを買ったら、でかすぎた件

f:id:ehu_J:20180726194746j:plain

壁掛け専用かーww

 

届く前まではこのサイズの半分くらいかと思ってたんですよ。それがこのサイズ。

壁から外して机に置いて書く想定だったのに、壁で書くしかなくなっちゃた...

 

因みに商品はこれ、

 まあ、このまま使いますよ...

次は、磁石で張り付くペンとイレイサーが欲しいな

【レビュー 】ブックストッパーで技術書が読みやすくなるのか?

こんにちは。ehu-151です。

 

 

 

今回は、技術書を読むときの便利アイテムとして、ブックストッパーを購入したのでレビューしたいと思います。

f:id:ehu_J:20180724235804j:plain

重さはスマホぐらいの質量です。クリップは先に丸みがあるので、紙が傷つくことはありません。

 

挟むページは大体100枚ほどが限界で30ページ挟むのがちょうどいいです。

f:id:ehu_J:20180725000149j:plain

 

下の写真は340ページある本です。真ん中のページあたりをクリップで挟みましたが、たるんでます。やはりページをしっかり開く系ではないようですね。

f:id:ehu_J:20180725000308j:plain

 

まだ満足してないので、ブックストッパーをまた買うかもしれません。

ではまた次回でー

【SF漫画2作品漫画紹介&機械学習】僕の好きな漫画

こんにちは、ehu-151です。

 

今回は自分の読んでいる漫画の紹介をさせていただきます。普段、プログラミングをやっているのにわざと脱線します。(ちゃんと後々、使う記事だから大丈夫。)

 

それでは紹介します。

 

はっぴぃヱンド(現在3巻まで)

 

 

1巻の表紙が物々しいww。そして右上に血がある上で「はっぴぃ」と名乗っています。全然説得力ないじゃん...。

 

まあ、とにかく、漫画のあらすじを読んでみましょう。

あらすじ

親の仕事の都合で
都会から田舎に引っ越してきた茜。

たった10人のクラスメートだけど、
とっても賑やかな学校生活!

魚釣りに行ったり、駄菓子屋で買い食いしたり、
自然に溢れた村で過ごす日々は毎日が新鮮で刺激的!

「こんな幸せな日常が永遠に続けばいい」と、
茜は心から思っていたのだが…!?

新進気鋭の鬼才が描く、
最高のしゅうまつのすごしかた! 

 3段落目までは、ほのぼの、日常系によくあるあらすじなのですが、4, 5段落目から不穏ですね。まずは、1話を読んでみてください。立ち読みで2話まで読めます。

gangan.square-enix.co.jp

どうでしょうか。「可愛い漫画」だと思いましたか?気になったらぜひ買ってみてください。

 

All You Need Is Kill(2巻 完結)

 

 

こちらはハリウッド映画化された漫画になります。死ぬとタイムリープするという現象にあいます。しかも、主人公は軍人で死と隣り合わせです。無事ループから抜け出すことができるのでしょうか?

 

通称ループものの部類になります。2巻ですが、短い時間でループものを読むときに最適です。

 

さいごに

qiita.com

 

機械学習で似ている漫画を予測しようという取り組みです。(失敗しましたが)