diff --git a/app/schemas/ai.elimu.content_provider.room.db.RoomDb/20.json b/app/schemas/ai.elimu.content_provider.room.db.RoomDb/20.json new file mode 100644 index 0000000..cbc5c16 --- /dev/null +++ b/app/schemas/ai.elimu.content_provider.room.db.RoomDb/20.json @@ -0,0 +1,503 @@ +{ + "formatVersion": 1, + "database": { + "version": 20, + "identityHash": "2c583ae048219fa9d33221907f4919b1", + "entities": [ + { + "tableName": "Letter", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`text` TEXT, `diacritic` INTEGER, `revisionNumber` INTEGER NOT NULL, `usageCount` INTEGER, `id` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "diacritic", + "columnName": "diacritic", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "revisionNumber", + "columnName": "revisionNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usageCount", + "columnName": "usageCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Word", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`text` TEXT NOT NULL, `wordType` TEXT, `revisionNumber` INTEGER NOT NULL, `usageCount` INTEGER, `id` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wordType", + "columnName": "wordType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "revisionNumber", + "columnName": "revisionNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usageCount", + "columnName": "usageCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Number", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` INTEGER NOT NULL, `symbol` TEXT, `revisionNumber` INTEGER NOT NULL, `usageCount` INTEGER, `id` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "revisionNumber", + "columnName": "revisionNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usageCount", + "columnName": "usageCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Emoji", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`glyph` TEXT NOT NULL, `unicodeVersion` REAL NOT NULL, `unicodeEmojiVersion` REAL NOT NULL, `revisionNumber` INTEGER NOT NULL, `usageCount` INTEGER, `id` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "glyph", + "columnName": "glyph", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unicodeVersion", + "columnName": "unicodeVersion", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "unicodeEmojiVersion", + "columnName": "unicodeEmojiVersion", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "revisionNumber", + "columnName": "revisionNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usageCount", + "columnName": "usageCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Emoji_Word", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`Emoji_id` INTEGER NOT NULL, `words_id` INTEGER NOT NULL, PRIMARY KEY(`Emoji_id`, `words_id`))", + "fields": [ + { + "fieldPath": "Emoji_id", + "columnName": "Emoji_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "words_id", + "columnName": "words_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "Emoji_id", + "words_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Image", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `imageFormat` TEXT NOT NULL, `revisionNumber` INTEGER NOT NULL, `usageCount` INTEGER, `id` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imageFormat", + "columnName": "imageFormat", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionNumber", + "columnName": "revisionNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usageCount", + "columnName": "usageCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Image_Word", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`Image_id` INTEGER NOT NULL, `words_id` INTEGER NOT NULL, PRIMARY KEY(`Image_id`, `words_id`))", + "fields": [ + { + "fieldPath": "Image_id", + "columnName": "Image_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "words_id", + "columnName": "words_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "Image_id", + "words_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StoryBook", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `description` TEXT, `coverImageId` INTEGER NOT NULL, `readingLevel` TEXT, `revisionNumber` INTEGER NOT NULL, `usageCount` INTEGER, `id` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverImageId", + "columnName": "coverImageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readingLevel", + "columnName": "readingLevel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "revisionNumber", + "columnName": "revisionNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usageCount", + "columnName": "usageCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StoryBookChapter", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`storyBookId` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL, `imageId` INTEGER NOT NULL, `id` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "storyBookId", + "columnName": "storyBookId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageId", + "columnName": "imageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StoryBookParagraph", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`storyBookChapterId` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL, `originalText` TEXT NOT NULL, `id` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "storyBookChapterId", + "columnName": "storyBookChapterId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originalText", + "columnName": "originalText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StoryBookParagraph_Word", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`StoryBookParagraph_id` INTEGER NOT NULL, `words_id` INTEGER NOT NULL, `words_ORDER` INTEGER NOT NULL, PRIMARY KEY(`StoryBookParagraph_id`, `words_ORDER`))", + "fields": [ + { + "fieldPath": "StoryBookParagraph_id", + "columnName": "StoryBookParagraph_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "words_id", + "columnName": "words_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "words_ORDER", + "columnName": "words_ORDER", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "StoryBookParagraph_id", + "words_ORDER" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Video", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `videoFormat` TEXT NOT NULL, `revisionNumber` INTEGER NOT NULL, `usageCount` INTEGER, `id` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "videoFormat", + "columnName": "videoFormat", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionNumber", + "columnName": "revisionNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usageCount", + "columnName": "usageCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2c583ae048219fa9d33221907f4919b1')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/content_provider/room/db/RoomDb.java b/app/src/main/java/ai/elimu/content_provider/room/db/RoomDb.java index 144bf52..4eacecc 100644 --- a/app/src/main/java/ai/elimu/content_provider/room/db/RoomDb.java +++ b/app/src/main/java/ai/elimu/content_provider/room/db/RoomDb.java @@ -38,7 +38,7 @@ import ai.elimu.content_provider.room.entity.Video; import ai.elimu.content_provider.room.entity.Word; -@Database(version = 19, entities = {Letter.class, Word.class, Number.class, Emoji.class, Emoji_Word.class, Image.class, Image_Word.class, StoryBook.class, StoryBookChapter.class, StoryBookParagraph.class, StoryBookParagraph_Word.class, Video.class}) +@Database(version = 20, entities = {Letter.class, Word.class, Number.class, Emoji.class, Emoji_Word.class, Image.class, Image_Word.class, StoryBook.class, StoryBookChapter.class, StoryBookParagraph.class, StoryBookParagraph_Word.class, Video.class}) @TypeConverters({Converters.class}) public abstract class RoomDb extends RoomDatabase { @@ -93,7 +93,8 @@ public static RoomDb getDatabase(final Context context) { MIGRATION_15_16, MIGRATION_16_17, MIGRATION_17_18, - MIGRATION_18_19 + MIGRATION_18_19, + MIGRATION_19_20 ) .build(); } @@ -250,4 +251,19 @@ public void migrate(SupportSQLiteDatabase database) { database.execSQL(sql); } }; + + private static final Migration MIGRATION_19_20 = new Migration(19, 20) { + @Override + public void migrate(SupportSQLiteDatabase database) { + Log.i(getClass().getName(), "migrate (19 --> 20)"); + + String sql = "DROP TABLE Image"; + Log.i(getClass().getName(), "sql: " + sql); + database.execSQL(sql); + + sql = "CREATE TABLE IF NOT EXISTS `Image` (`title` TEXT NOT NULL, `imageFormat` TEXT NOT NULL, `revisionNumber` INTEGER NOT NULL, `usageCount` INTEGER, `id` INTEGER, PRIMARY KEY(`id`))"; + Log.i(getClass().getName(), "sql: " + sql); + database.execSQL(sql); + } + }; } diff --git a/app/src/main/java/ai/elimu/content_provider/room/entity/Image.java b/app/src/main/java/ai/elimu/content_provider/room/entity/Image.java index ab2105a..5265780 100644 --- a/app/src/main/java/ai/elimu/content_provider/room/entity/Image.java +++ b/app/src/main/java/ai/elimu/content_provider/room/entity/Image.java @@ -1,7 +1,6 @@ package ai.elimu.content_provider.room.entity; import androidx.annotation.NonNull; -import androidx.room.ColumnInfo; import androidx.room.Entity; import ai.elimu.model.enums.content.ImageFormat; @@ -15,10 +14,6 @@ public class Image extends Content { @NonNull private String title; - @NonNull - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - private byte[] bytes; - @NonNull private ImageFormat imageFormat; @@ -30,14 +25,6 @@ public void setTitle(String title) { this.title = title; } - public byte[] getBytes() { - return bytes; - } - - public void setBytes(byte[] bytes) { - this.bytes = bytes; - } - public ImageFormat getImageFormat() { return imageFormat; } diff --git a/app/src/main/java/ai/elimu/content_provider/ui/image/ImagesFragment.java b/app/src/main/java/ai/elimu/content_provider/ui/image/ImagesFragment.java index ec8d31d..32b7144 100644 --- a/app/src/main/java/ai/elimu/content_provider/ui/image/ImagesFragment.java +++ b/app/src/main/java/ai/elimu/content_provider/ui/image/ImagesFragment.java @@ -16,6 +16,10 @@ import com.google.android.material.snackbar.Snackbar; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -30,6 +34,7 @@ import ai.elimu.content_provider.room.db.RoomDb; import ai.elimu.content_provider.room.entity.Image; import ai.elimu.content_provider.room.entity.Image_Word; +import ai.elimu.content_provider.util.FileHelper; import ai.elimu.content_provider.util.MultimediaDownloader; import ai.elimu.model.v2.gson.content.ImageGson; import ai.elimu.model.v2.gson.content.WordGson; @@ -119,19 +124,36 @@ public void run() { // Empty the database table before downloading up-to-date content image_WordDao.deleteAll(); imageDao.deleteAll(); + // TODO: also delete corresponding image files (only those that are no longer used) for (ImageGson imageGson : imageGsons) { Log.i(getClass().getName(), "imageGson.getId(): " + imageGson.getId()); Image image = GsonToRoomConverter.getImage(imageGson); - // Download bytes - BaseApplication baseApplication = (BaseApplication) getActivity().getApplication(); - String downloadUrl = baseApplication.getBaseUrl() + imageGson.getBytesUrl(); - Log.i(getClass().getName(), "downloadUrl: " + downloadUrl); - byte[] bytes = MultimediaDownloader.downloadFileBytes(downloadUrl); - Log.i(getClass().getName(), "bytes.length: " + bytes.length); - image.setBytes(bytes); + // Check if the corresponding image file has already been downloaded + File imageFile = FileHelper.getImageFile(imageGson, getContext()); + Log.i(getClass().getName(), "imageFile: " + imageFile); + Log.i(getClass().getName(), "imageFile.exists(): " + imageFile.exists()); + if (!imageFile.exists()) { + // Download file bytes + BaseApplication baseApplication = (BaseApplication) getActivity().getApplication(); + String downloadUrl = baseApplication.getBaseUrl() + imageGson.getBytesUrl(); + Log.i(getClass().getName(), "downloadUrl: " + downloadUrl); + byte[] bytes = MultimediaDownloader.downloadFileBytes(downloadUrl); + Log.i(getClass().getName(), "bytes.length: " + bytes.length); + + // Store the downloaded file in the external storage directory + try { + FileOutputStream fileOutputStream = new FileOutputStream(imageFile); + fileOutputStream.write(bytes); + } catch (FileNotFoundException e) { + Log.e(getClass().getName(), null, e); + } catch (IOException e) { + Log.e(getClass().getName(), null, e); + } + Log.i(getClass().getName(), "imageFile.exists(): " + imageFile.exists()); + } // Store the Image in the database imageDao.insert(image); diff --git a/app/src/main/java/ai/elimu/content_provider/util/FileHelper.java b/app/src/main/java/ai/elimu/content_provider/util/FileHelper.java index c0c56a8..98ec737 100644 --- a/app/src/main/java/ai/elimu/content_provider/util/FileHelper.java +++ b/app/src/main/java/ai/elimu/content_provider/util/FileHelper.java @@ -6,6 +6,7 @@ import ai.elimu.content_provider.language.SharedPreferencesHelper; import ai.elimu.model.enums.Language; +import ai.elimu.model.v2.gson.content.ImageGson; import ai.elimu.model.v2.gson.content.VideoGson; /** @@ -13,6 +14,27 @@ */ public class FileHelper { + private static File getImagesDirectory(Context context) { + File externalFilesDir = context.getExternalFilesDir(null); + Language language = SharedPreferencesHelper.getLanguage(context); + File languageDirectory = new File(externalFilesDir, "lang-" + language.getIsoCode()); + File imagesDirectory = new File(languageDirectory, "images"); + if (!imagesDirectory.exists()) { + imagesDirectory.mkdirs(); + } + return imagesDirectory; + } + + public static File getImageFile(ImageGson imageGson, Context context) { + if ((imageGson.getId() == null) || (imageGson.getRevisionNumber() == null)) { + return null; + } + File imagesDirectory = getImagesDirectory(context); + File file = new File(imagesDirectory, imageGson.getId() + "_r" + imageGson.getRevisionNumber() + "." + imageGson.getImageFormat().toString().toLowerCase()); + return file; + } + + private static File getVideosDirectory(Context context) { File externalFilesDir = context.getExternalFilesDir(null); Language language = SharedPreferencesHelper.getLanguage(context);