diff --git a/app/build.gradle b/app/build.gradle index 4e7d063..407d1de 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,19 +2,16 @@ buildscript { repositories { mavenCentral() } - dependencies { - classpath libs.green.dao.gradle.plugin - } } plugins { alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.kapt) } import java.util.regex.Matcher import java.util.regex.Pattern apply plugin: 'com.android.application' -apply plugin: 'org.greenrobot.greendao' apply plugin: 'maven-publish' android { @@ -54,25 +51,26 @@ android { } } -greendao { - schemaVersion android.defaultConfig.versionCode - targetGenDir = new File('app/src/main/java') - daoPackage = android.defaultConfig.applicationId + '.dao' -} - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation libs.elimu.content.provider // https://jitpack.io/#ai.elimu/content-provider - implementation libs.green.dao implementation libs.circle.imageview implementation libs.androidx.core.ktx + implementation libs.androidx.activity.ktx + + implementation libs.room.runtime + implementation libs.room.ktx + kapt libs.room.compiler + + implementation libs.javapoet testImplementation libs.junit androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.espresso + } publishing { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5cff74e..9946225 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ android:theme="@style/AppTheme"> diff --git a/app/src/main/java/ai/elimu/chat/ChatActivity.kt b/app/src/main/java/ai/elimu/chat/ChatActivity.kt deleted file mode 100644 index 11e5b41..0000000 --- a/app/src/main/java/ai/elimu/chat/ChatActivity.kt +++ /dev/null @@ -1,302 +0,0 @@ -package ai.elimu.chat - -import ai.elimu.chat.dao.MessageDao -import ai.elimu.chat.model.Message -import ai.elimu.chat.receiver.StudentUpdateReceiver -import ai.elimu.chat.util.DeviceInfoHelper -import android.app.Activity -import android.os.Bundle -import android.preference.PreferenceManager -import android.text.Editable -import android.text.TextUtils -import android.text.TextWatcher -import android.util.Log -import android.view.View -import android.widget.ArrayAdapter -import android.widget.EditText -import android.widget.ImageButton -import android.widget.ListView -import java.util.Calendar - -class ChatActivity : Activity() { - private var messageDao: MessageDao? = null - - private var messages: MutableList = mutableListOf() - - private var arrayAdapter: ArrayAdapter<*>? = null - - private var mListPreviousMessages: ListView? = null - - private var messageText: EditText? = null - - private var mButtonSend: ImageButton? = null - - override fun onCreate(savedInstanceState: Bundle?) { - Log.i(javaClass.getName(), "onCreate") - super.onCreate(savedInstanceState) - - setContentView(R.layout.activity_chat) - - messageDao = (application as ChatApplication).daoSession!!.messageDao - - mListPreviousMessages = findViewById(R.id.listPreviousMessages) as ListView - messageText = findViewById(R.id.message) as EditText - mButtonSend = findViewById(R.id.buttonSend) as ImageButton - } - - override fun onStart() { - Log.i(javaClass.getName(), "onStart") - super.onStart() - - // ContentProvider.initializeDb(this); -// List letters = ContentProvider.getAvailableLetters(); -// Log.i(getClass().getName(), "letters: " + letters); - - // Load messages sent within the last 24 hours - val calendar24HoursAgo = Calendar.getInstance() - calendar24HoursAgo.add(Calendar.HOUR_OF_DAY, -24) - messages = messageDao!!.queryBuilder() - .where(MessageDao.Properties.TimeSent.gt(calendar24HoursAgo.getTimeInMillis())) - .list() - Log.i(javaClass.getName(), "messages.size(): " + messages!!.size) - - arrayAdapter = MessageListArrayAdapter(applicationContext, messages) - mListPreviousMessages!!.setAdapter(arrayAdapter) - - messageText!!.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence?, i: Int, i1: Int, i2: Int) { - } - - override fun onTextChanged(charSequence: CharSequence?, i: Int, i1: Int, i2: Int) { - } - - override fun afterTextChanged(editable: Editable?) { - Log.i(javaClass.getName(), "afterTextChanged") - - Log.i(javaClass.getName(), "editable: $editable") - - if (!TextUtils.isEmpty(editable)) { - if (!mButtonSend!!.isEnabled) { - mButtonSend!!.setEnabled(true) - mButtonSend!!.setImageDrawable(getDrawable(R.drawable.ic_send_white_24dp)) - - // // Animate button to indicate that it can be pressed -// final long duration = 300; -// -// final ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(mButtonSend, View.SCALE_X, 1f, 1.2f, 1f); -// scaleXAnimator.setDuration(duration); -// scaleXAnimator.setRepeatCount(1); -// -// final ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(mButtonSend, View.SCALE_Y, 1f, 1.2f, 1f); -// scaleYAnimator.setDuration(duration); -// scaleYAnimator.setRepeatCount(1); -// -// scaleXAnimator.start(); -// scaleYAnimator.start(); -// -// final AnimatorSet animatorSet = new AnimatorSet(); -// animatorSet.play(scaleXAnimator).with(scaleYAnimator); -// animatorSet.start(); - } - } else { - mButtonSend!!.setEnabled(false) - mButtonSend!!.setImageDrawable(getDrawable(R.drawable.ic_send_grey_24dp)) - } - } - }) - - // Default to grey button - mButtonSend!!.setEnabled(false) - mButtonSend!!.setImageDrawable(getDrawable(R.drawable.ic_send_grey_24dp)) - - mButtonSend!!.setOnClickListener(object : View.OnClickListener { - override fun onClick(view: View?) { - Log.i(javaClass.getName(), "mButtonSend onClick") - - val text = messageText!!.getText().toString() - Log.i(javaClass.getName(), "text: $text") - - val sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(applicationContext) - - // Store in database - val message = Message() - message.deviceId = DeviceInfoHelper.getDeviceId(applicationContext) - val studentId = - sharedPreferences.getString(StudentUpdateReceiver.PREF_STUDENT_ID, null) - if (!TextUtils.isEmpty(studentId)) { - message.studentId = studentId - } - val studentAvatar = - sharedPreferences.getString(StudentUpdateReceiver.PREF_STUDENT_AVATAR, null) - if (!TextUtils.isEmpty(studentAvatar)) { - message.studentAvatar = studentAvatar - } - message.timeSent = Calendar.getInstance() - message.text = text - messageDao!!.insert(message) - - // Add to UI - addToMessageListAndRefresh(message) - - // Reset input field - messageText!!.setText("") - - val showMessageFromAkili = Math.random() > 0.5 - if (showMessageFromAkili) { - mButtonSend!!.postDelayed(object : Runnable { - override fun run() { - // Simulate message from AI tutor - - // Akili - - val message = Message() - message.studentId = "00000000aaaaaaaa_1" - message.timeSent = Calendar.getInstance() - message.text = randomEmoji - addToMessageListAndRefresh(message) - } - }, (2000 + (Math.random() * 8000).toInt()).toLong()) - } - - val showMessageFromPenguin = Math.random() > 0.5 - if (showMessageFromPenguin) { - mButtonSend!!.postDelayed(object : Runnable { - override fun run() { - // Penguin - val messagePenguin = Message() - messagePenguin.studentId = "00000000aaaaaaaa_2" - messagePenguin.timeSent = Calendar.getInstance() - messagePenguin.text = randomEmoji - addToMessageListAndRefresh(messagePenguin) - } - }, (2000 + (Math.random() * 8000).toInt()).toLong()) - } - } - }) - } - - private fun addToMessageListAndRefresh(message: Message) { - Log.i(javaClass.getName(), "addToMessageListAndRefresh") - messages.add(message) - refreshMessageList() - } - - private fun refreshMessageList() { - Log.i(javaClass.getName(), "refreshMessageList") - - arrayAdapter!!.notifyDataSetChanged() - mListPreviousMessages!!.smoothScrollToPosition(mListPreviousMessages!!.count) - // Fix problem with scrolling when keyboard is present - mListPreviousMessages!!.postDelayed(object : Runnable { - override fun run() { - Log.i(javaClass.getName(), "mListPreviousMessages.postDelayed") - mListPreviousMessages!!.setSelection(mListPreviousMessages!!.count) - } - }, 100) - } - - private val randomEmoji: String - get() { - val unicodes = intArrayOf( - // Emoticons - 0x1F601, - 0x1F602, - 0x1F603, - 0x1F604, - 0x1F605, - 0x1F606, - 0x1F609, - 0x1F60A, - 0x1F60B, - 0x1F60C, - 0x1F60D, - 0x1F60F, - 0x1F612, - 0x1F613, - 0x1F614, - 0x1F616, - 0x1F618, - 0x1F61A, - 0x1F61C, - 0x1F61D, - 0x1F61E, - 0x1F620, - 0x1F621, - 0x1F622, - 0x1F623, - 0x1F624, - 0x1F625, - 0x1F628, - 0x1F629, - 0x1F62A, - 0x1F62B, - 0x1F62D, - 0x1F630, - 0x1F631, - 0x1F632, - 0x1F633, - 0x1F635, - 0x1F637, - 0x1F638, - 0x1F639, - 0x1F63A, - 0x1F63B, - 0x1F63C, - 0x1F63D, - 0x1F63E, - 0x1F63F, - 0x1F640, - 0x1F645, - 0x1F646, - 0x1F647, - 0x1F648, - 0x1F649, - 0x1F64A, - 0x1F64B, - 0x1F64C, - 0x1F64D, - 0x1F64E, - 0x1F64F, // Uncategorized - - 0x1F40C, - 0x1F40D, - 0x1F40E, - 0x1F411, - 0x1F412, - 0x1F414, - 0x1F418, - 0x1F419, - 0x1F41A, - 0x1F41B, - 0x1F41C, - 0x1F41D, - 0x1F41E, - 0x1F41F, - 0x1F420, - 0x1F421, - 0x1F422, - 0x1F423, - 0x1F424, - 0x1F425, - 0x1F426, - 0x1F427, - 0x1F428, - ) - val randomIndex = (Math.random() * unicodes.size).toInt() - val unicode = unicodes[randomIndex] - Log.d(javaClass.getName(), "unicode: $unicode") - val emoji = getEmijoByUnicode(unicode) - Log.i(javaClass.getName(), "emoji: $emoji") - return emoji - } - - /** - * See http://apps.timwhitlock.info/emoji/tables/unicode - * @param unicode Example: "U+1F601" --> "0x1F601" - * @return - */ - private fun getEmijoByUnicode(unicode: Int): String { - return String(Character.toChars(unicode)) - } -} diff --git a/app/src/main/java/ai/elimu/chat/ChatApplication.kt b/app/src/main/java/ai/elimu/chat/ChatApplication.kt index cd6a786..24201c5 100644 --- a/app/src/main/java/ai/elimu/chat/ChatApplication.kt +++ b/app/src/main/java/ai/elimu/chat/ChatApplication.kt @@ -1,48 +1,45 @@ package ai.elimu.chat -import ai.elimu.chat.dao.DaoMaster -import ai.elimu.chat.dao.DaoMaster.DevOpenHelper -import ai.elimu.chat.dao.DaoSession + +import ai.elimu.chat.di.ServiceLocator +import ai.elimu.chat.util.Constants import ai.elimu.chat.util.VersionHelper import android.app.Application -import android.preference.PreferenceManager import android.util.Log import androidx.core.content.edit class ChatApplication : Application() { - var daoSession: DaoSession? = null - private set override fun onCreate() { Log.i(javaClass.getName(), "onCreate") super.onCreate() + ServiceLocator.initialize(this) + checkAndUpdateVersionCode() + } - val helper = DevOpenHelper(this, "chat-db") - val db = helper.writableDb - daoSession = DaoMaster(db).newSession() - + private fun checkAndUpdateVersionCode() { // Check if the application's versionCode was upgraded - val sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(applicationContext) - var oldVersionCode = sharedPreferences.getInt(PREF_APP_VERSION_CODE, 0) + val sharedPreferences = ServiceLocator.provideSharedPreference() + val oldVersionCode = sharedPreferences.getInt(Constants.PREF_APP_VERSION_CODE, 0) val newVersionCode = VersionHelper.getAppVersionCode(applicationContext) - if (oldVersionCode == 0) { - sharedPreferences.edit(commit = true) { putInt(PREF_APP_VERSION_CODE, newVersionCode) } - oldVersionCode = newVersionCode - } if (oldVersionCode < newVersionCode) { Log.i( javaClass.getName(), "Upgrading application from version $oldVersionCode to $newVersionCode" ) - // if (newVersionCode == ???) { -// // Put relevant tasks required for upgrading here -// } - sharedPreferences.edit(commit = true) { putInt(PREF_APP_VERSION_CODE, newVersionCode) } + sharedPreferences.edit(commit = true) { + putInt( + Constants.PREF_APP_VERSION_CODE, + newVersionCode + ) + } + } else if (oldVersionCode == 0) { + sharedPreferences.edit(commit = true) { + putInt( + Constants.PREF_APP_VERSION_CODE, + newVersionCode + ) + } } } - - companion object { - const val PREF_APP_VERSION_CODE: String = "pref_app_version_code" - } } diff --git a/app/src/main/java/ai/elimu/chat/dao/DaoSession.java b/app/src/main/java/ai/elimu/chat/dao/DaoSession.java deleted file mode 100644 index b5e3ff9..0000000 --- a/app/src/main/java/ai/elimu/chat/dao/DaoSession.java +++ /dev/null @@ -1,48 +0,0 @@ -package ai.elimu.chat.dao; - -import java.util.Map; - -import org.greenrobot.greendao.AbstractDao; -import org.greenrobot.greendao.AbstractDaoSession; -import org.greenrobot.greendao.database.Database; -import org.greenrobot.greendao.identityscope.IdentityScopeType; -import org.greenrobot.greendao.internal.DaoConfig; - -import ai.elimu.chat.model.Message; - -import ai.elimu.chat.dao.MessageDao; - -// THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. - -/** - * {@inheritDoc} - * - * @see org.greenrobot.greendao.AbstractDaoSession - */ -public class DaoSession extends AbstractDaoSession { - - private final DaoConfig messageDaoConfig; - - private final MessageDao messageDao; - - public DaoSession(Database db, IdentityScopeType type, Map>, DaoConfig> - daoConfigMap) { - super(db); - - messageDaoConfig = daoConfigMap.get(MessageDao.class).clone(); - messageDaoConfig.initIdentityScope(type); - - messageDao = new MessageDao(messageDaoConfig, this); - - registerDao(Message.class, messageDao); - } - - public void clear() { - messageDaoConfig.clearIdentityScope(); - } - - public MessageDao getMessageDao() { - return messageDao; - } - -} diff --git a/app/src/main/java/ai/elimu/chat/dao/MessageDao.java b/app/src/main/java/ai/elimu/chat/dao/MessageDao.java deleted file mode 100644 index b98de70..0000000 --- a/app/src/main/java/ai/elimu/chat/dao/MessageDao.java +++ /dev/null @@ -1,164 +0,0 @@ -package ai.elimu.chat.dao; - -import android.database.Cursor; -import android.database.sqlite.SQLiteStatement; - -import org.greenrobot.greendao.AbstractDao; -import org.greenrobot.greendao.Property; -import org.greenrobot.greendao.internal.DaoConfig; -import org.greenrobot.greendao.database.Database; -import org.greenrobot.greendao.database.DatabaseStatement; - -import ai.elimu.chat.dao.converter.CalendarConverter; - -import ai.elimu.chat.model.Message; - -// THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. -/** - * DAO for table "MESSAGE". -*/ -public class MessageDao extends AbstractDao { - - public static final String TABLENAME = "MESSAGE"; - - /** - * Properties of entity Message.
- * Can be used for QueryBuilder and for referencing column names. - */ - public static class Properties { - public final static Property Id = new Property(0, Long.class, "id", true, "_id"); - public final static Property DeviceId = new Property(1, String.class, "deviceId", false, "DEVICE_ID"); - public final static Property StudentId = new Property(2, String.class, "studentId", false, "STUDENT_ID"); - public final static Property StudentAvatar = new Property(3, String.class, "studentAvatar", false, "STUDENT_AVATAR"); - public final static Property TimeSent = new Property(4, long.class, "timeSent", false, "TIME_SENT"); - public final static Property Text = new Property(5, String.class, "text", false, "TEXT"); - } - - private final CalendarConverter timeSentConverter = new CalendarConverter(); - - public MessageDao(DaoConfig config) { - super(config); - } - - public MessageDao(DaoConfig config, DaoSession daoSession) { - super(config, daoSession); - } - - /** Creates the underlying database table. */ - public static void createTable(Database db, boolean ifNotExists) { - String constraint = ifNotExists? "IF NOT EXISTS ": ""; - db.execSQL("CREATE TABLE " + constraint + "\"MESSAGE\" (" + // - "\"_id\" INTEGER PRIMARY KEY AUTOINCREMENT ," + // 0: id - "\"DEVICE_ID\" TEXT NOT NULL ," + // 1: deviceId - "\"STUDENT_ID\" TEXT," + // 2: studentId - "\"STUDENT_AVATAR\" TEXT," + // 3: studentAvatar - "\"TIME_SENT\" INTEGER NOT NULL ," + // 4: timeSent - "\"TEXT\" TEXT NOT NULL );"); // 5: text - } - - /** Drops the underlying database table. */ - public static void dropTable(Database db, boolean ifExists) { - String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"MESSAGE\""; - db.execSQL(sql); - } - - @Override - protected final void bindValues(DatabaseStatement stmt, Message entity) { - stmt.clearBindings(); - - Long id = entity.getId(); - if (id != null) { - stmt.bindLong(1, id); - } - stmt.bindString(2, entity.getDeviceId()); - - String studentId = entity.getStudentId(); - if (studentId != null) { - stmt.bindString(3, studentId); - } - - String studentAvatar = entity.getStudentAvatar(); - if (studentAvatar != null) { - stmt.bindString(4, studentAvatar); - } - stmt.bindLong(5, timeSentConverter.convertToDatabaseValue(entity.getTimeSent())); - stmt.bindString(6, entity.getText()); - } - - @Override - protected final void bindValues(SQLiteStatement stmt, Message entity) { - stmt.clearBindings(); - - Long id = entity.getId(); - if (id != null) { - stmt.bindLong(1, id); - } - stmt.bindString(2, entity.getDeviceId()); - - String studentId = entity.getStudentId(); - if (studentId != null) { - stmt.bindString(3, studentId); - } - - String studentAvatar = entity.getStudentAvatar(); - if (studentAvatar != null) { - stmt.bindString(4, studentAvatar); - } - stmt.bindLong(5, timeSentConverter.convertToDatabaseValue(entity.getTimeSent())); - stmt.bindString(6, entity.getText()); - } - - @Override - public Long readKey(Cursor cursor, int offset) { - return cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0); - } - - @Override - public Message readEntity(Cursor cursor, int offset) { - Message entity = new Message( // - cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id - cursor.getString(offset + 1), // deviceId - cursor.isNull(offset + 2) ? null : cursor.getString(offset + 2), // studentId - cursor.isNull(offset + 3) ? null : cursor.getString(offset + 3), // studentAvatar - timeSentConverter.convertToEntityProperty(cursor.getLong(offset + 4)), // timeSent - cursor.getString(offset + 5) // text - ); - return entity; - } - - @Override - public void readEntity(Cursor cursor, Message entity, int offset) { - entity.setId(cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0)); - entity.setDeviceId(cursor.getString(offset + 1)); - entity.setStudentId(cursor.isNull(offset + 2) ? null : cursor.getString(offset + 2)); - entity.setStudentAvatar(cursor.isNull(offset + 3) ? null : cursor.getString(offset + 3)); - entity.setTimeSent(timeSentConverter.convertToEntityProperty(cursor.getLong(offset + 4))); - entity.setText(cursor.getString(offset + 5)); - } - - @Override - protected final Long updateKeyAfterInsert(Message entity, long rowId) { - entity.setId(rowId); - return rowId; - } - - @Override - public Long getKey(Message entity) { - if(entity != null) { - return entity.getId(); - } else { - return null; - } - } - - @Override - public boolean hasKey(Message entity) { - return entity.getId() != null; - } - - @Override - protected final boolean isEntityUpdateable() { - return true; - } - -} diff --git a/app/src/main/java/ai/elimu/chat/dao/converter/CalendarConverter.kt b/app/src/main/java/ai/elimu/chat/dao/converter/CalendarConverter.kt deleted file mode 100644 index fc69fbf..0000000 --- a/app/src/main/java/ai/elimu/chat/dao/converter/CalendarConverter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package ai.elimu.chat.dao.converter - -import org.greenrobot.greendao.converter.PropertyConverter -import java.util.Calendar - -class CalendarConverter : PropertyConverter { - - override fun convertToEntityProperty(databaseValue: Long?): Calendar? { - val calendar = Calendar.getInstance() - databaseValue?.let { - calendar.setTimeInMillis(databaseValue) - } - return calendar - } - - override fun convertToDatabaseValue(entityProperty: Calendar?): Long? { - val databaseValue = entityProperty?.getTimeInMillis() - return databaseValue - } -} diff --git a/app/src/main/java/ai/elimu/chat/data/local/AppDatabase.kt b/app/src/main/java/ai/elimu/chat/data/local/AppDatabase.kt new file mode 100644 index 0000000..072f04e --- /dev/null +++ b/app/src/main/java/ai/elimu/chat/data/local/AppDatabase.kt @@ -0,0 +1,12 @@ +package ai.elimu.chat.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database( + entities = [MessageEntity::class], + version = 1 +) +abstract class AppDatabase : RoomDatabase() { + abstract fun messageDao(): ChatMessageDao +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/chat/data/local/ChatMessageDao.kt b/app/src/main/java/ai/elimu/chat/data/local/ChatMessageDao.kt new file mode 100644 index 0000000..83d1d27 --- /dev/null +++ b/app/src/main/java/ai/elimu/chat/data/local/ChatMessageDao.kt @@ -0,0 +1,30 @@ +package ai.elimu.chat.data.local + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query + +@Dao +interface ChatMessageDao { + @Insert + suspend fun insert(message: MessageEntity): Long + + @Query( + "SELECT * FROM messages WHERE device_id LIKE :deviceId AND" + + " student_id LIKE :studentId" + ) + suspend fun getAllMessages(deviceId: String, studentId: String): List + + @Query("SELECT * FROM messages WHERE timestamp > :timeStamp") + suspend fun getMessages(timeStamp: Long): List + + @Query( + "UPDATE messages SET student_id = :studentId, " + + "student_avatar = :studentAvatar WHERE device_id = :deviceId AND student_id IS NULL" + ) + suspend fun updateStudentInfoForDevice( + deviceId: String, + studentId: String?, + studentAvatar: String? + ) +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/chat/data/local/MessageEntity.kt b/app/src/main/java/ai/elimu/chat/data/local/MessageEntity.kt new file mode 100644 index 0000000..e6666aa --- /dev/null +++ b/app/src/main/java/ai/elimu/chat/data/local/MessageEntity.kt @@ -0,0 +1,16 @@ +package ai.elimu.chat.data.local + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "messages") + +data class MessageEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo(name = "device_id") val deviceId: String, + @ColumnInfo(name = "student_id") val studentId: String?, + @ColumnInfo(name = "student_avatar") val studentAvatar: String?, + @ColumnInfo(name = "message") val text: String, + @ColumnInfo(name = "timestamp") val timeSent: Long +) diff --git a/app/src/main/java/ai/elimu/chat/data/local/MessageMapper.kt b/app/src/main/java/ai/elimu/chat/data/local/MessageMapper.kt new file mode 100644 index 0000000..a643b4b --- /dev/null +++ b/app/src/main/java/ai/elimu/chat/data/local/MessageMapper.kt @@ -0,0 +1,25 @@ +package ai.elimu.chat.data.local + +import ai.elimu.chat.model.Message +import java.util.Calendar + +fun MessageEntity.toMessage(): Message { + return Message( + this.id, + this.deviceId, + this.studentId, + this.studentAvatar, + Calendar.getInstance().apply { timeInMillis = this@toMessage.timeSent }, + this.text + ) +} + +fun Message.toEntity(): MessageEntity { + return MessageEntity( + deviceId = this.deviceId, + studentId = this.studentId, + studentAvatar = this.studentAvatar, + timeSent = this.timeSent.timeInMillis, + text = this.text + ) +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/chat/di/ServiceLocator.kt b/app/src/main/java/ai/elimu/chat/di/ServiceLocator.kt new file mode 100644 index 0000000..9296b7e --- /dev/null +++ b/app/src/main/java/ai/elimu/chat/di/ServiceLocator.kt @@ -0,0 +1,46 @@ +package ai.elimu.chat.di + +import ai.elimu.chat.data.local.AppDatabase +import ai.elimu.chat.data.local.ChatMessageDao +import ai.elimu.chat.util.Constants +import android.annotation.SuppressLint +import android.content.Context +import android.content.SharedPreferences +import android.provider.Settings +import androidx.room.Room + +object ServiceLocator { + + private var chatMessageDao: ChatMessageDao? = null + + private var deviceId: String? = null + + private var sharedPreferences: SharedPreferences? = null + + @SuppressLint("HardwareIds") + fun initialize(context: Context) { + val db = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "chat-db-new" + ).build() + chatMessageDao = db.messageDao() + + //TODO: Need to change this + deviceId = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) + + sharedPreferences = context.getSharedPreferences(Constants.PREF_FILE_NAME, Context.MODE_PRIVATE) + } + + fun provideChatMessageDao(): ChatMessageDao { + return chatMessageDao ?: throw IllegalStateException("ServiceLocator not initialized") + } + + fun provideDeviceId(): String { + return deviceId ?: throw IllegalStateException("ServiceLocator not initialized") + } + + fun provideSharedPreference(): SharedPreferences { + return sharedPreferences ?: throw IllegalStateException("ServiceLocator not initialized") + } +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/chat/model/Message.java b/app/src/main/java/ai/elimu/chat/model/Message.java deleted file mode 100644 index 6b9fc1f..0000000 --- a/app/src/main/java/ai/elimu/chat/model/Message.java +++ /dev/null @@ -1,95 +0,0 @@ -package ai.elimu.chat.model; - -import org.greenrobot.greendao.annotation.Convert; -import org.greenrobot.greendao.annotation.Entity; -import org.greenrobot.greendao.annotation.Generated; -import org.greenrobot.greendao.annotation.Id; -import org.greenrobot.greendao.annotation.NotNull; -import ai.elimu.chat.dao.converter.CalendarConverter; - -import java.util.Calendar; - -@Entity -public class Message { - - @Id(autoincrement = true) - private Long id; - - @NotNull - private String deviceId; - - private String studentId; - - private String studentAvatar; - - @NotNull - @Convert(converter = CalendarConverter.class, columnType = Long.class) - private Calendar timeSent; - - @NotNull - private String text; - - @Generated(hash = 449884345) - public Message(Long id, @NotNull String deviceId, String studentId, - String studentAvatar, @NotNull Calendar timeSent, - @NotNull String text) { - this.id = id; - this.deviceId = deviceId; - this.studentId = studentId; - this.studentAvatar = studentAvatar; - this.timeSent = timeSent; - this.text = text; - } - - @Generated(hash = 637306882) - public Message() { - } - - public Long getId() { - return this.id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getDeviceId() { - return this.deviceId; - } - - public void setDeviceId(String deviceId) { - this.deviceId = deviceId; - } - - public String getStudentId() { - return this.studentId; - } - - public void setStudentId(String studentId) { - this.studentId = studentId; - } - - public String getStudentAvatar() { - return this.studentAvatar; - } - - public void setStudentAvatar(String studentAvatar) { - this.studentAvatar = studentAvatar; - } - - public Calendar getTimeSent() { - return this.timeSent; - } - - public void setTimeSent(Calendar timeSent) { - this.timeSent = timeSent; - } - - public String getText() { - return this.text; - } - - public void setText(String text) { - this.text = text; - } -} diff --git a/app/src/main/java/ai/elimu/chat/model/Message.kt b/app/src/main/java/ai/elimu/chat/model/Message.kt new file mode 100644 index 0000000..ae9c54f --- /dev/null +++ b/app/src/main/java/ai/elimu/chat/model/Message.kt @@ -0,0 +1,51 @@ +package ai.elimu.chat.model + +import ai.elimu.chat.util.Constants.EMOJI_UNICODES +import java.util.Calendar + +data class Message( + val id: Long, + val deviceId: String, + val studentId: String?, + val studentAvatar: String?, + val timeSent: Calendar, + val text: String +) + +class MessageBuilder { + private var id: Long = 0 + private var deviceId: String = "" + private var studentId: String? = null + private var studentAvatar: String? = null + private var timeSent: Calendar = Calendar.getInstance() + private var text: String = "" + + fun id(id: Long) = apply { this.id = id } + fun deviceId(deviceId: String) = apply { this.deviceId = deviceId } + fun studentId(studentId: String?) = apply { this.studentId = studentId } + fun studentAvatar(studentAvatar: String?) = apply { this.studentAvatar = studentAvatar } + fun timeSent(timesent: Calendar) = apply { this.timeSent = timesent } + fun message(text: String) = apply { this.text = text } + + fun build() = Message(id, deviceId, studentId, studentAvatar, timeSent, text) +} + +fun generateEmojiMessage(studentId: String): Message { + val message = MessageBuilder() + message.studentId(studentId) + message.message(getRandomEmoji()) + return message.build() +} + +private fun getRandomEmoji(): String { + val randomIndex = (Math.random() * EMOJI_UNICODES.size).toInt() + val unicode = EMOJI_UNICODES[randomIndex] + + /** + * See http://apps.timwhitlock.info/emoji/tables/unicode + * @param unicode Example: "U+1F601" --> "0x1F601" + * @return + */ + val emoji = String(Character.toChars(unicode)) + return emoji +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/chat/receiver/StudentUpdateReceiver.kt b/app/src/main/java/ai/elimu/chat/receiver/StudentUpdateReceiver.kt index 98303eb..bb2ce82 100644 --- a/app/src/main/java/ai/elimu/chat/receiver/StudentUpdateReceiver.kt +++ b/app/src/main/java/ai/elimu/chat/receiver/StudentUpdateReceiver.kt @@ -1,12 +1,10 @@ package ai.elimu.chat.receiver -import ai.elimu.chat.ChatApplication -import ai.elimu.chat.dao.MessageDao -import ai.elimu.chat.util.DeviceInfoHelper.getDeviceId +import ai.elimu.chat.di.ServiceLocator +import ai.elimu.chat.util.Constants import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.preference.PreferenceManager import android.text.TextUtils import android.util.Log import androidx.core.content.edit @@ -14,46 +12,56 @@ import androidx.core.content.edit class StudentUpdateReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Log.i(javaClass.getName(), "onReceive") - - val studentId = intent.getStringExtra("studentId") + val studentId = intent.getStringExtra(EXTRA_STUDENT_ID) Log.i(javaClass.getName(), "studentId: $studentId") - val studentAvatar = intent.getStringExtra("studentAvatar") + val studentAvatar = intent.getStringExtra(EXTRA_STUDENT_AVATAR) Log.i(javaClass.getName(), "studentAvatar: $studentAvatar") - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + val sharedPreferences = ServiceLocator.provideSharedPreference() if (!TextUtils.isEmpty(studentId)) { - val existingStudentId = sharedPreferences.getString(PREF_STUDENT_ID, null) + val existingStudentId = sharedPreferences.getString(Constants.PREF_STUDENT_ID, null) Log.i(javaClass.getName(), "existingStudentId: $existingStudentId") - if (TextUtils.isEmpty(existingStudentId)) { - // Update previously sent messages on the current device - val chatApplication = context.applicationContext as ChatApplication - val messageDao = chatApplication.daoSession!!.messageDao - val existingMessages = messageDao.queryBuilder() - .where( - MessageDao.Properties.DeviceId.eq(getDeviceId(context)), - MessageDao.Properties.StudentId.isNull() - ) - .list() - Log.i(javaClass.getName(), "existingMessages.size(): " + existingMessages.size) - for (message in existingMessages) { - message.studentId = studentId - message.studentAvatar = studentAvatar - messageDao.update(message) - } - } + /* if (TextUtils.isEmpty(existingStudentId)) { + // Update previously sent messages on the current device // TODO: Migrate to room + val chatApplication = context.applicationContext as ChatApplication + val messageDao = chatApplication.daoSession!!.messageDao + val existingMessages = messageDao.queryBuilder() + .where( + MessageDao.Properties.DeviceId.eq(getDeviceId(context)), + MessageDao.Properties.StudentId.isNull() + ) + .list() + Log.i(javaClass.getName(), "existingMessages.size(): " + existingMessages.size) + for (message in existingMessages) { + message.studentId = studentId + message.studentAvatar = studentAvatar + messageDao.update(message) + } + }*/ - sharedPreferences.edit(commit = true) { putString(PREF_STUDENT_ID, studentId) } + sharedPreferences.edit(commit = true) { + putString( + Constants.PREF_STUDENT_ID, + studentId + ) + } } if (!TextUtils.isEmpty(studentAvatar)) { - sharedPreferences.edit(commit = true) { putString(PREF_STUDENT_AVATAR, studentAvatar) } + sharedPreferences.edit(commit = true) { + putString( + Constants.PREF_STUDENT_AVATAR, + studentAvatar + ) + } } } companion object { - const val PREF_STUDENT_ID: String = "pref_student_id" - const val PREF_STUDENT_AVATAR: String = "pref_student_avatar" + const val EXTRA_STUDENT_ID = "studentId" + + const val EXTRA_STUDENT_AVATAR = "studentAvatar" } } diff --git a/app/src/main/java/ai/elimu/chat/ui/ChatActivity.kt b/app/src/main/java/ai/elimu/chat/ui/ChatActivity.kt new file mode 100644 index 0000000..2ca69e4 --- /dev/null +++ b/app/src/main/java/ai/elimu/chat/ui/ChatActivity.kt @@ -0,0 +1,118 @@ +package ai.elimu.chat.ui + +import ai.elimu.chat.R +import ai.elimu.chat.model.Message +import ai.elimu.chat.util.Constants +import android.annotation.SuppressLint +import android.os.Bundle +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.util.Log +import android.view.View +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.ImageButton +import android.widget.ListView +import androidx.core.app.ComponentActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch + +@SuppressLint("RestrictedApi") +class ChatActivity : ComponentActivity() { + + private var messages: MutableList = mutableListOf() + + private lateinit var arrayAdapter: ArrayAdapter<*> + + private lateinit var mListPreviousMessages: ListView + + private lateinit var messageText: EditText + + private lateinit var mButtonSend: ImageButton + + lateinit var viewModel: ChatViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + Log.i(javaClass.getName(), "onCreate") + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_chat) + + viewModel = ChatViewModel() + + mListPreviousMessages = findViewById(R.id.listPreviousMessages) as ListView + messageText = findViewById(R.id.message) as EditText + mButtonSend = findViewById(R.id.buttonSend) as ImageButton + + arrayAdapter = MessageListArrayAdapter(applicationContext, messages) + mListPreviousMessages.setAdapter(arrayAdapter) + + viewModel.loadRecentMessages() + + lifecycleScope.launch { + viewModel.messages.collect { newMessages -> + messages.clear() + messages.addAll(newMessages) + refreshMessageList() + } + } + + messageText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence?, i: Int, i1: Int, i2: Int) { + } + + override fun onTextChanged(charSequence: CharSequence?, i: Int, i1: Int, i2: Int) { + } + + override fun afterTextChanged(editable: Editable?) { + Log.i(javaClass.getName(), "afterTextChanged") + setSendButtonState(TextUtils.isEmpty(editable)) + } + }) + + setSendButtonState(false) + + mButtonSend.setOnClickListener { + Log.i(javaClass.getName(), "mButtonSend onClick") + val message = messageText.getText().toString() + sendMessage(message) + } + } + + private fun sendMessage(message: String) { + viewModel.sendMessage(message) + + // Reset input field + messageText.setText("") + + //showMessageFromAkili + viewModel.maybeSimulateMessage(Constants.STUDENTID_AKILI) + + //showMessageFromPenguin + viewModel.maybeSimulateMessage(Constants.STUDENTID_PENGUIN) + } + + private fun setSendButtonState(isEmpty: Boolean) { + mButtonSend.apply { + isEnabled = !isEmpty + setImageDrawable( + getDrawable( + if (isEmpty) R.drawable.ic_send_grey_24dp else R.drawable.ic_send_white_24dp + ) + ) + } + } + + private fun refreshMessageList() { + Log.i(javaClass.getName(), "refreshMessageList") + + arrayAdapter.notifyDataSetChanged() + mListPreviousMessages.smoothScrollToPosition(mListPreviousMessages.count) + // Fix problem with scrolling when keyboard is present + mListPreviousMessages.postDelayed({ + Log.i(javaClass.getName(), "mListPreviousMessages.postDelayed") + mListPreviousMessages.setSelection(mListPreviousMessages.count) + }, 100) + } +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/chat/ui/ChatViewModel.kt b/app/src/main/java/ai/elimu/chat/ui/ChatViewModel.kt new file mode 100644 index 0000000..fc84b59 --- /dev/null +++ b/app/src/main/java/ai/elimu/chat/ui/ChatViewModel.kt @@ -0,0 +1,74 @@ +package ai.elimu.chat.ui + +import ai.elimu.chat.data.local.toEntity +import ai.elimu.chat.data.local.toMessage +import ai.elimu.chat.di.ServiceLocator +import ai.elimu.chat.model.Message +import ai.elimu.chat.model.MessageBuilder +import ai.elimu.chat.model.generateEmojiMessage +import ai.elimu.chat.util.Constants +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import java.util.Calendar +import kotlin.random.Random + +class ChatViewModel() : ViewModel() { + private val _messages = MutableStateFlow>(emptyList()) + + val messages: StateFlow> = _messages + + val sharedPreferences = ServiceLocator.provideSharedPreference() + + val chatMessageDao = ServiceLocator.provideChatMessageDao() + + fun loadRecentMessages() { + viewModelScope.launch { + val calendar24HoursAgo = Calendar.getInstance() + calendar24HoursAgo.add(Calendar.HOUR_OF_DAY, -24) + val newMessages = + chatMessageDao.getMessages(timeStamp = calendar24HoursAgo.timeInMillis) + val uiMessages = newMessages.map { it.toMessage() } + _messages.value = emptyList() + _messages.value = uiMessages + } + } + + fun sendMessage(message: String) { + val messageBuilder = MessageBuilder() + messageBuilder.deviceId(ServiceLocator.provideDeviceId()) + val studentId = + sharedPreferences.getString(Constants.PREF_STUDENT_ID, null) + studentId?.let { + messageBuilder.studentId(it) + } + + val studentAvatar = + sharedPreferences.getString(Constants.PREF_STUDENT_AVATAR, null) + studentAvatar?.let { + messageBuilder.studentAvatar(it) + } + + messageBuilder.message(message) + + viewModelScope.launch { + val message = messageBuilder.build() + chatMessageDao.insert(message.toEntity()) + _messages.value = _messages.value + message + } + } + + fun maybeSimulateMessage(studentId: String) { + if (Random.nextBoolean()) { + val delayMillis = (2000..10000).random().toLong() + viewModelScope.launch { + delay(delayMillis) + val message = generateEmojiMessage(studentId) + _messages.value = _messages.value + message + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/chat/MessageListArrayAdapter.kt b/app/src/main/java/ai/elimu/chat/ui/MessageListArrayAdapter.kt similarity index 88% rename from app/src/main/java/ai/elimu/chat/MessageListArrayAdapter.kt rename to app/src/main/java/ai/elimu/chat/ui/MessageListArrayAdapter.kt index 61b3676..83970e7 100644 --- a/app/src/main/java/ai/elimu/chat/MessageListArrayAdapter.kt +++ b/app/src/main/java/ai/elimu/chat/ui/MessageListArrayAdapter.kt @@ -1,10 +1,11 @@ -package ai.elimu.chat +package ai.elimu.chat.ui +import ai.elimu.chat.R +import ai.elimu.chat.di.ServiceLocator import ai.elimu.chat.model.Message -import ai.elimu.chat.receiver.StudentUpdateReceiver +import ai.elimu.chat.util.Constants import android.content.Context import android.graphics.BitmapFactory -import android.preference.PreferenceManager import android.text.TextUtils import android.util.Log import android.view.LayoutInflater @@ -34,8 +35,7 @@ class MessageListArrayAdapter(context: Context, messages: List) : this.context = context this.messages = messages - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - studentId = sharedPreferences.getString(StudentUpdateReceiver.PREF_STUDENT_ID, null) + studentId = ServiceLocator.provideSharedPreference().getString(Constants.PREF_STUDENT_ID, null) } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { @@ -43,7 +43,7 @@ class MessageListArrayAdapter(context: Context, messages: List) : val message = messages[position] - var listItem: View? = null + var listItem: View? val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater @@ -76,4 +76,4 @@ class MessageListArrayAdapter(context: Context, messages: List) : return listItem } -} +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/chat/util/Constants.kt b/app/src/main/java/ai/elimu/chat/util/Constants.kt new file mode 100644 index 0000000..21bd5cd --- /dev/null +++ b/app/src/main/java/ai/elimu/chat/util/Constants.kt @@ -0,0 +1,102 @@ +package ai.elimu.chat.util + +object Constants { + + const val PREF_FILE_NAME: String = "pref_app_001" + + const val PREF_APP_VERSION_CODE: String = "pref_app_version_code" + + const val PREF_STUDENT_ID: String = "pref_student_id" + + const val PREF_STUDENT_AVATAR: String = "pref_student_avatar" + + const val STUDENTID_AKILI = "00000000aaaaaaaa_1" + + const val STUDENTID_PENGUIN = "00000000aaaaaaaa_2" + + val EMOJI_UNICODES = intArrayOf( + // Emoticons + 0x1F601, + 0x1F602, + 0x1F603, + 0x1F604, + 0x1F605, + 0x1F606, + 0x1F609, + 0x1F60A, + 0x1F60B, + 0x1F60C, + 0x1F60D, + 0x1F60F, + 0x1F612, + 0x1F613, + 0x1F614, + 0x1F616, + 0x1F618, + 0x1F61A, + 0x1F61C, + 0x1F61D, + 0x1F61E, + 0x1F620, + 0x1F621, + 0x1F622, + 0x1F623, + 0x1F624, + 0x1F625, + 0x1F628, + 0x1F629, + 0x1F62A, + 0x1F62B, + 0x1F62D, + 0x1F630, + 0x1F631, + 0x1F632, + 0x1F633, + 0x1F635, + 0x1F637, + 0x1F638, + 0x1F639, + 0x1F63A, + 0x1F63B, + 0x1F63C, + 0x1F63D, + 0x1F63E, + 0x1F63F, + 0x1F640, + 0x1F645, + 0x1F646, + 0x1F647, + 0x1F648, + 0x1F649, + 0x1F64A, + 0x1F64B, + 0x1F64C, + 0x1F64D, + 0x1F64E, + 0x1F64F, // Uncategorized + + 0x1F40C, + 0x1F40D, + 0x1F40E, + 0x1F411, + 0x1F412, + 0x1F414, + 0x1F418, + 0x1F419, + 0x1F41A, + 0x1F41B, + 0x1F41C, + 0x1F41D, + 0x1F41E, + 0x1F41F, + 0x1F420, + 0x1F421, + 0x1F422, + 0x1F423, + 0x1F424, + 0x1F425, + 0x1F426, + 0x1F427, + 0x1F428, + ) +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/chat/util/DeviceInfoHelper.kt b/app/src/main/java/ai/elimu/chat/util/DeviceInfoHelper.kt deleted file mode 100644 index e2c829f..0000000 --- a/app/src/main/java/ai/elimu/chat/util/DeviceInfoHelper.kt +++ /dev/null @@ -1,11 +0,0 @@ -package ai.elimu.chat.util - -import android.content.Context -import android.provider.Settings - -object DeviceInfoHelper { - @JvmStatic - fun getDeviceId(context: Context): String? { - return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d754aaa..26617e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,27 +1,35 @@ [versions] elimuContentProvider = "1.4.51" agp = "8.6.0" +javapoet = "1.13.0" junit = "4.13.2" -greenDao = "3.3.0" -greenDaoGradlePlugin = "3.3.1" circleImageView = "3.1.0" androidXJunit = "1.2.1" androidXEspresso = "3.7.0" coreKtx = "1.16.0" +activityKtx="1.10.1" kotlin = "2.2.0" +room = "2.7.2" [libraries] elimu-content-provider = { group = "ai.elimu", name = "content-provider", version.ref = "elimuContentProvider" } -green-dao = { group = "org.greenrobot", name = "greendao", version.ref = "greenDao" } -green-dao-gradle-plugin = { group = "org.greenrobot", name = "greendao-gradle-plugin", version.ref = "greenDaoGradlePlugin" } circle-imageview = { group = "de.hdodenhof", name = "circleimageview", version.ref = "circleImageView" } agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidXJunit" } androidx-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidXEspresso" } +javapoet = { module = "com.squareup:javapoet", version.ref = "javapoet" } +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } + junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" } + + [plugins] -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } \ No newline at end of file +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } \ No newline at end of file