diff --git a/app/build.gradle b/app/build.gradle index a803818e..5a6611dc 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,6 +17,12 @@ android { vectorDrawables.useSupportLibrary = true vectorDrawables.generatedDensities = [] + + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } compileOptions { @@ -32,8 +38,13 @@ android { aaptOptions { additionalParameters '--no-version-vectors' } buildTypes { + debug { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } @@ -45,19 +56,19 @@ configurations.all { } dependencies { - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.0' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' def appcompat_version = "1.2.0" - def nav_version = '2.3.1' + def nav_version = '2.3.2' - implementation 'com.google.android.material:material:1.3.0-alpha03' + implementation 'com.google.android.material:material:1.3.0-alpha04' implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0' implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0' implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" - implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06" + implementation "androidx.recyclerview:recyclerview:1.2.0-beta01" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.navigation:navigation-fragment:$nav_version" @@ -69,6 +80,12 @@ dependencies { implementation 'com.google.guava:guava:27.0.1-android' + // Room + def room_version = "2.2.5" + implementation "androidx.room:room-runtime:$room_version" + implementation "androidx.room:room-guava:$room_version" + annotationProcessor "androidx.room:room-compiler:$room_version" + // implementation 'com.github.hendrawd:StorageUtil:1.1.0' implementation 'com.github.ammargitham:AutoLinkTextViewV2:master-SNAPSHOT' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b42451..aeb70430 100755 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,10 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +#noinspection ShrinkerUnresolvedReference +#-keep class !com.google.android.exoplayer2.**, ** { *; } + +#-keep class !awais.instagrabber.** { *; } + +-dontobfuscate \ No newline at end of file diff --git a/app/schemas/awais.instagrabber.db.AppDatabase/4.json b/app/schemas/awais.instagrabber.db.AppDatabase/4.json new file mode 100644 index 00000000..528d203b --- /dev/null +++ b/app/schemas/awais.instagrabber.db.AppDatabase/4.json @@ -0,0 +1,114 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "538d64adaeb8c3a98db9204955932e59", + "entities": [ + { + "tableName": "accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `username` TEXT, `cookie` TEXT, `full_name` TEXT, `profile_pic` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "profilePic", + "columnName": "profile_pic", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_text` TEXT, `type` TEXT, `display_name` TEXT, `pic_url` TEXT, `date_added` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "picUrl", + "columnName": "pic_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateAdded", + "columnName": "date_added", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, '538d64adaeb8c3a98db9204955932e59')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d33cda0..556de192 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -108,7 +108,8 @@ + android:parentActivityName=".activities.MainActivity" + android:theme="@style/AppTheme.Light.White"> diff --git a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java index 5f87ed0e..874b425f 100644 --- a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java +++ b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java @@ -13,7 +13,6 @@ import java.text.SimpleDateFormat; import java.util.UUID; import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.DataBox; import awais.instagrabber.utils.LocaleUtils; import awais.instagrabber.utils.SettingsHelper; import awaisomereport.CrashReporter; @@ -21,7 +20,6 @@ import awaisomereport.LogCollector; import static awais.instagrabber.utils.CookieUtils.NET_COOKIE_MANAGER; import static awais.instagrabber.utils.Utils.clipboardManager; -import static awais.instagrabber.utils.Utils.dataBox; import static awais.instagrabber.utils.Utils.datetimeParser; import static awais.instagrabber.utils.Utils.logCollector; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -58,11 +56,6 @@ public final class InstaGrabberApplication extends Application { CookieHandler.setDefault(NET_COOKIE_MANAGER); - final Context appContext = getApplicationContext(); - - if (dataBox == null) - dataBox = DataBox.getInstance(appContext); - if (settingsHelper == null) settingsHelper = new SettingsHelper(this); diff --git a/app/src/main/java/awais/instagrabber/activities/DirectDownload.java b/app/src/main/java/awais/instagrabber/activities/DirectDownload.java index 4d675b3e..448aab78 100644 --- a/app/src/main/java/awais/instagrabber/activities/DirectDownload.java +++ b/app/src/main/java/awais/instagrabber/activities/DirectDownload.java @@ -26,9 +26,11 @@ import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.IntentModel; import awais.instagrabber.models.enums.IntentModelType; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.IntentUtils; import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; public final class DirectDownload extends AppCompatActivity { private static final int NOTIFICATION_ID = 1900000000; @@ -88,6 +90,7 @@ public final class DirectDownload extends AppCompatActivity { } private synchronized void doDownload() { + CookieUtils.setupCookies(Utils.settingsHelper.getString(Constants.COOKIE)); notificationManager = NotificationManagerCompat.from(getApplicationContext()); final Intent intent = getIntent(); final String action = intent.getAction(); diff --git a/app/src/main/java/awais/instagrabber/adapters/AccountSwitcherAdapter.java b/app/src/main/java/awais/instagrabber/adapters/AccountSwitcherAdapter.java index 8e984ddc..10cd73dc 100644 --- a/app/src/main/java/awais/instagrabber/adapters/AccountSwitcherAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/AccountSwitcherAdapter.java @@ -14,21 +14,21 @@ import androidx.recyclerview.widget.RecyclerView; import awais.instagrabber.R; import awais.instagrabber.databinding.PrefAccountSwitcherBinding; +import awais.instagrabber.db.entities.Account; import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.DataBox; import static awais.instagrabber.utils.Utils.settingsHelper; -public class AccountSwitcherAdapter extends ListAdapter { +public class AccountSwitcherAdapter extends ListAdapter { private static final String TAG = "AccountSwitcherAdapter"; - private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override - public boolean areItemsTheSame(@NonNull final DataBox.CookieModel oldItem, @NonNull final DataBox.CookieModel newItem) { + public boolean areItemsTheSame(@NonNull final Account oldItem, @NonNull final Account newItem) { return oldItem.getUid().equals(newItem.getUid()); } @Override - public boolean areContentsTheSame(@NonNull final DataBox.CookieModel oldItem, @NonNull final DataBox.CookieModel newItem) { + public boolean areContentsTheSame(@NonNull final Account oldItem, @NonNull final Account newItem) { return oldItem.getUid().equals(newItem.getUid()); } }; @@ -53,7 +53,7 @@ public class AccountSwitcherAdapter extends ListAdapter { @@ -102,7 +102,7 @@ public class FavoritesAdapter extends RecyclerView.Adapter list) { + public void submitList(@Nullable final List list) { if (list == null) { differ.submitList(null); return; @@ -110,7 +110,7 @@ public class FavoritesAdapter extends RecyclerView.Adapter list, @Nullable final Runnable commitCallback) { + public void submitList(@Nullable final List list, @Nullable final Runnable commitCallback) { if (list == null) { differ.submitList(null, commitCallback); return; @@ -119,8 +119,8 @@ public class FavoritesAdapter extends RecyclerView.Adapter sectionAndSort(@NonNull final List list) { - final List listCopy = new ArrayList<>(list); + private List sectionAndSort(@NonNull final List list) { + final List listCopy = new ArrayList<>(list); Collections.sort(listCopy, (o1, o2) -> { if (o1.getType() == o2.getType()) return 0; // keep users at top @@ -133,7 +133,7 @@ public class FavoritesAdapter extends RecyclerView.Adapter modelOrHeaders = new ArrayList<>(); for (int i = 0; i < listCopy.size(); i++) { - final DataBox.FavoriteModel model = listCopy.get(i); + final Favorite model = listCopy.get(i); final FavoriteModelOrHeader prev = modelOrHeaders.isEmpty() ? null : modelOrHeaders.get(modelOrHeaders.size() - 1); boolean prevWasSameType = prev != null && prev.model.getType() == model.getType(); if (prevWasSameType) { @@ -156,7 +156,7 @@ public class FavoritesAdapter extends RecyclerView.Adapter> { private static final String TAG = "NotificationsFetcher"; @@ -37,8 +35,6 @@ public final class NotificationsFetcher extends AsyncTask doInBackground(final Void... voids) { List result = new ArrayList<>(); final String url = "https://www.instagram.com/accounts/activity/?__a=1"; - CookieUtils.setupCookies(settingsHelper.getString(Constants.COOKIE)); - try { final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setInstanceFollowRedirects(false); diff --git a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java index 39ba0d9e..f87e5103 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java @@ -17,11 +17,9 @@ import awais.instagrabber.models.PostChild; import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.NetworkUtils; import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; import awaisomereport.LogCollector; import static awais.instagrabber.utils.Utils.logCollector; @@ -39,7 +37,6 @@ public final class PostFetcher extends AsyncTask { @Override protected FeedModel doInBackground(final Void... voids) { - CookieUtils.setupCookies(Utils.settingsHelper.getString(Constants.COOKIE)); // <- direct download HttpURLConnection conn = null; try { conn = (HttpURLConnection) new URL("https://www.instagram.com/p/" + shortCode + "/?__a=1").openConnection(); diff --git a/app/src/main/java/awais/instagrabber/db/AppDatabase.java b/app/src/main/java/awais/instagrabber/db/AppDatabase.java new file mode 100644 index 00000000..6190575a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/AppDatabase.java @@ -0,0 +1,229 @@ +package awais.instagrabber.db; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import androidx.room.TypeConverters; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import awais.instagrabber.db.dao.AccountDao; +import awais.instagrabber.db.dao.FavoriteDao; +import awais.instagrabber.db.entities.Account; +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.models.enums.FavoriteType; +import awais.instagrabber.utils.Utils; + +@Database(entities = {Account.class, Favorite.class}, + version = 4) +@TypeConverters({Converters.class}) +public abstract class AppDatabase extends RoomDatabase { + private static final String TAG = AppDatabase.class.getSimpleName(); + + private static AppDatabase INSTANCE; + + public abstract AccountDao accountDao(); + + public abstract FavoriteDao favoriteDao(); + + public static AppDatabase getDatabase(final Context context) { + if (INSTANCE == null) { + synchronized (AppDatabase.class) { + if (INSTANCE == null) { + INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "cookiebox.db") + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) + .build(); + } + } + } + return INSTANCE; + } + + static final Migration MIGRATION_1_2 = new Migration(1, 2) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase db) { + db.execSQL("ALTER TABLE cookies ADD " + Account.COL_FULL_NAME + " TEXT"); + db.execSQL("ALTER TABLE cookies ADD " + Account.COL_PROFILE_PIC + " TEXT"); + } + }; + + static final Migration MIGRATION_2_3 = new Migration(2, 3) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase db) { + final List oldFavorites = backupOldFavorites(db); + // recreate with new columns (as there will be no doubt about the `query_display` column being present or not in the future versions) + db.execSQL("DROP TABLE " + Favorite.TABLE_NAME); + db.execSQL("CREATE TABLE " + Favorite.TABLE_NAME + " (" + + Favorite.COL_ID + " INTEGER PRIMARY KEY," + + Favorite.COL_QUERY + " TEXT," + + Favorite.COL_TYPE + " TEXT," + + Favorite.COL_DISPLAY_NAME + " TEXT," + + Favorite.COL_PIC_URL + " TEXT," + + Favorite.COL_DATE_ADDED + " INTEGER)"); + // add the old favorites back + for (final Favorite oldFavorite : oldFavorites) { + insertOrUpdateFavorite(db, oldFavorite); + } + } + }; + + static final Migration MIGRATION_3_4 = new Migration(3, 4) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase db) { + // Required when migrating to Room. + // The original table primary keys were not 'NOT NULL', so the migration to Room were failing without the below migration. + // Taking this opportunity to rename cookies table to accounts + + // Create new table with name 'accounts' + db.execSQL("CREATE TABLE " + Account.TABLE_NAME + " (" + + Account.COL_ID + " INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + + Account.COL_UID + " TEXT," + + Account.COL_USERNAME + " TEXT," + + Account.COL_COOKIE + " TEXT," + + Account.COL_FULL_NAME + " TEXT," + + Account.COL_PROFILE_PIC + " TEXT)"); + // Insert all data from table 'cookies' to 'accounts' + db.execSQL("INSERT INTO " + Account.TABLE_NAME + " (" + + Account.COL_UID + "," + + Account.COL_USERNAME + "," + + Account.COL_COOKIE + "," + + Account.COL_FULL_NAME + "," + + Account.COL_PROFILE_PIC + ") " + + "SELECT " + + Account.COL_UID + "," + + Account.COL_USERNAME + "," + + Account.COL_COOKIE + "," + + Account.COL_FULL_NAME + "," + + Account.COL_PROFILE_PIC + + " FROM cookies"); + // Drop old cookies table + db.execSQL("DROP TABLE cookies"); + + // Create favorite backup table + db.execSQL("CREATE TABLE " + Favorite.TABLE_NAME + "_backup (" + + Favorite.COL_ID + " INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + + Favorite.COL_QUERY + " TEXT," + + Favorite.COL_TYPE + " TEXT," + + Favorite.COL_DISPLAY_NAME + " TEXT," + + Favorite.COL_PIC_URL + " TEXT," + + Favorite.COL_DATE_ADDED + " INTEGER)"); + // Insert all data from table 'favorite' to 'favorite_backup' + db.execSQL("INSERT INTO " + Favorite.TABLE_NAME + "_backup (" + + Favorite.COL_QUERY + "," + + Favorite.COL_TYPE + "," + + Favorite.COL_DISPLAY_NAME + "," + + Favorite.COL_PIC_URL + "," + + Favorite.COL_DATE_ADDED + ") " + + "SELECT " + + Favorite.COL_QUERY + "," + + Favorite.COL_TYPE + "," + + Favorite.COL_DISPLAY_NAME + "," + + Favorite.COL_PIC_URL + "," + + Favorite.COL_DATE_ADDED + + " FROM " + Favorite.TABLE_NAME); + // Drop favorites + db.execSQL("DROP TABLE " + Favorite.TABLE_NAME); + // Rename favorite_backup to favorites + db.execSQL("ALTER TABLE " + Favorite.TABLE_NAME + "_backup RENAME TO " + Favorite.TABLE_NAME); + } + }; + + @NonNull + private static List backupOldFavorites(@NonNull final SupportSQLiteDatabase db) { + // check if old favorites table had the column query_display + final boolean queryDisplayExists = checkColumnExists(db, Favorite.TABLE_NAME, "query_display"); + Log.d(TAG, "backupOldFavorites: queryDisplayExists: " + queryDisplayExists); + final List oldModels = new ArrayList<>(); + final String sql = "SELECT " + + "query_text," + + "date_added" + + (queryDisplayExists ? ",query_display" : "") + + " FROM " + Favorite.TABLE_NAME; + try (final Cursor cursor = db.query(sql)) { + if (cursor != null && cursor.moveToFirst()) { + do { + try { + final String queryText = cursor.getString(cursor.getColumnIndex("query_text")); + final Pair favoriteTypeQueryPair = Utils.migrateOldFavQuery(queryText); + if (favoriteTypeQueryPair == null) continue; + final FavoriteType type = favoriteTypeQueryPair.first; + final String query = favoriteTypeQueryPair.second; + oldModels.add(new Favorite( + -1, + query, + type, + queryDisplayExists ? cursor.getString(cursor.getColumnIndex("query_display")) + : null, + null, + new Date(cursor.getLong(cursor.getColumnIndex("date_added"))) + )); + } catch (Exception e) { + Log.e(TAG, "onUpgrade", e); + } + } while (cursor.moveToNext()); + } + } catch (Exception e) { + Log.e(TAG, "onUpgrade", e); + } + Log.d(TAG, "backupOldFavorites: oldModels:" + oldModels); + return oldModels; + } + + private static synchronized void insertOrUpdateFavorite(@NonNull final SupportSQLiteDatabase db, @NonNull final Favorite model) { + final ContentValues values = new ContentValues(); + values.put(Favorite.COL_QUERY, model.getQuery()); + values.put(Favorite.COL_TYPE, model.getType().toString()); + values.put(Favorite.COL_DISPLAY_NAME, model.getDisplayName()); + values.put(Favorite.COL_PIC_URL, model.getPicUrl()); + values.put(Favorite.COL_DATE_ADDED, model.getDateAdded().getTime()); + int rows; + if (model.getId() >= 1) { + rows = db.update(Favorite.TABLE_NAME, + SQLiteDatabase.CONFLICT_IGNORE, + values, + Favorite.COL_ID + "=?", + new String[]{String.valueOf(model.getId())}); + } else { + rows = db.update(Favorite.TABLE_NAME, + SQLiteDatabase.CONFLICT_IGNORE, + values, + Favorite.COL_QUERY + "=?" + " AND " + Favorite.COL_TYPE + "=?", + new String[]{model.getQuery(), model.getType().toString()}); + } + if (rows != 1) { + db.insert(Favorite.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values); + } + } + + private static boolean checkColumnExists(@NonNull final SupportSQLiteDatabase db, + @NonNull final String tableName, + @NonNull final String columnName) { + boolean exists = false; + try (Cursor cursor = db.query("PRAGMA table_info(" + tableName + ")")) { + if (cursor.moveToFirst()) { + do { + final String currentColumn = cursor.getString(cursor.getColumnIndex("name")); + if (currentColumn.equals(columnName)) { + exists = true; + } + } while (cursor.moveToNext()); + + } + } catch (Exception ex) { + Log.e(TAG, "checkColumnExists", ex); + } + return exists; + } +} diff --git a/app/src/main/java/awais/instagrabber/db/Converters.java b/app/src/main/java/awais/instagrabber/db/Converters.java new file mode 100644 index 00000000..50252755 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/Converters.java @@ -0,0 +1,33 @@ +package awais.instagrabber.db; + +import androidx.room.TypeConverter; + +import java.util.Date; + +import awais.instagrabber.models.enums.FavoriteType; + +public class Converters { + @TypeConverter + public static Date fromTimestamp(Long value) { + return value == null ? null : new Date(value); + } + + @TypeConverter + public static Long dateToTimestamp(Date date) { + return date == null ? null : date.getTime(); + } + + @TypeConverter + public static FavoriteType fromFavoriteTypeString(String value) { + try { + return FavoriteType.valueOf(value); + } catch (Exception e) { + return null; + } + } + + @TypeConverter + public static String favoriteTypeToString(FavoriteType favoriteType) { + return favoriteType == null ? null : favoriteType.toString(); + } +} diff --git a/app/src/main/java/awais/instagrabber/db/dao/AccountDao.java b/app/src/main/java/awais/instagrabber/db/dao/AccountDao.java new file mode 100644 index 00000000..8524f192 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/dao/AccountDao.java @@ -0,0 +1,34 @@ +package awais.instagrabber.db.dao; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; +import androidx.room.Update; + +import java.util.List; + +import awais.instagrabber.db.entities.Account; + +@Dao +public interface AccountDao { + + @Query("SELECT * FROM accounts") + List getAllAccounts(); + + @Query("SELECT * FROM accounts WHERE uid = :uid") + Account findAccountByUid(String uid); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + List insertAccounts(Account... accounts); + + @Update + void updateAccounts(Account... accounts); + + @Delete + void deleteAccounts(Account... accounts); + + @Query("DELETE from accounts") + void deleteAllAccounts(); +} diff --git a/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.java b/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.java new file mode 100644 index 00000000..50dc3129 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.java @@ -0,0 +1,35 @@ +package awais.instagrabber.db.dao; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; +import androidx.room.Update; + +import java.util.List; + +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.models.enums.FavoriteType; + +@Dao +public interface FavoriteDao { + + @Query("SELECT * FROM favorites") + List getAllFavorites(); + + @Query("SELECT * FROM favorites WHERE query_text = :query and type = :type") + Favorite findFavoriteByQueryAndType(String query, FavoriteType type); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + List insertFavorites(Favorite... favorites); + + @Update + void updateFavorites(Favorite... favorites); + + @Delete + void deleteFavorites(Favorite... favorites); + + @Query("DELETE from favorites") + void deleteAllFavorites(); +} diff --git a/app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.java b/app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.java new file mode 100644 index 00000000..a2ffe515 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.java @@ -0,0 +1,68 @@ +package awais.instagrabber.db.datasources; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; + +import awais.instagrabber.db.AppDatabase; +import awais.instagrabber.db.dao.AccountDao; +import awais.instagrabber.db.entities.Account; + +public class AccountDataSource { + private static final String TAG = AccountDataSource.class.getSimpleName(); + + private static AccountDataSource INSTANCE; + + private final AccountDao accountDao; + + private AccountDataSource(final AccountDao accountDao) { + this.accountDao = accountDao; + } + + public static AccountDataSource getInstance(@NonNull Context context) { + if (INSTANCE == null) { + synchronized (AccountDataSource.class) { + if (INSTANCE == null) { + final AppDatabase database = AppDatabase.getDatabase(context); + INSTANCE = new AccountDataSource(database.accountDao()); + } + } + } + return INSTANCE; + } + + @Nullable + public final Account getAccount(final String uid) { + return accountDao.findAccountByUid(uid); + } + + @NonNull + public final List getAllAccounts() { + return accountDao.getAllAccounts(); + } + + public final void insertOrUpdateAccount(final String uid, + final String username, + final String cookie, + final String fullName, + final String profilePicUrl) { + final Account account = getAccount(uid); + final Account toUpdate = new Account(account == null ? 0 : account.getId(), uid, username, cookie, fullName, profilePicUrl); + if (account != null) { + accountDao.updateAccounts(toUpdate); + return; + } + accountDao.insertAccounts(toUpdate); + } + + public final void deleteAccount(@NonNull final Account account) { + accountDao.deleteAccounts(account); + } + + public final void deleteAllAccounts() { + accountDao.deleteAllAccounts(); + } +} diff --git a/app/src/main/java/awais/instagrabber/db/datasources/FavoriteDataSource.java b/app/src/main/java/awais/instagrabber/db/datasources/FavoriteDataSource.java new file mode 100644 index 00000000..36798b08 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/datasources/FavoriteDataSource.java @@ -0,0 +1,62 @@ +package awais.instagrabber.db.datasources; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; + +import awais.instagrabber.db.AppDatabase; +import awais.instagrabber.db.dao.FavoriteDao; +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.models.enums.FavoriteType; + +public class FavoriteDataSource { + private static final String TAG = FavoriteDataSource.class.getSimpleName(); + + private static FavoriteDataSource INSTANCE; + + private final FavoriteDao favoriteDao; + + private FavoriteDataSource(final FavoriteDao favoriteDao) { + this.favoriteDao = favoriteDao; + } + + public static synchronized FavoriteDataSource getInstance(@NonNull Context context) { + if (INSTANCE == null) { + synchronized (FavoriteDataSource.class) { + if (INSTANCE == null) { + final AppDatabase database = AppDatabase.getDatabase(context); + INSTANCE = new FavoriteDataSource(database.favoriteDao()); + } + } + } + return INSTANCE; + } + + + @Nullable + public final Favorite getFavorite(@NonNull final String query, @NonNull final FavoriteType type) { + return favoriteDao.findFavoriteByQueryAndType(query, type); + } + + @NonNull + public final List getAllFavorites() { + return favoriteDao.getAllFavorites(); + } + + public final void insertOrUpdateFavorite(@NonNull final Favorite favorite) { + if (favorite.getId() > 0) { + favoriteDao.updateFavorites(favorite); + return; + } + favoriteDao.insertFavorites(favorite); + } + + public final void deleteFavorite(@NonNull final String query, @NonNull final FavoriteType type) { + final Favorite favorite = getFavorite(query, type); + if (favorite == null) return; + favoriteDao.deleteFavorites(favorite); + } +} diff --git a/app/src/main/java/awais/instagrabber/db/entities/Account.java b/app/src/main/java/awais/instagrabber/db/entities/Account.java new file mode 100644 index 00000000..57faa716 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/entities/Account.java @@ -0,0 +1,124 @@ +package awais.instagrabber.db.entities; + +import androidx.annotation.NonNull; +import androidx.core.util.ObjectsCompat; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.PrimaryKey; + +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.TextUtils; + +@Entity(tableName = Account.TABLE_NAME) +public class Account { + public final static String TABLE_NAME = "accounts"; + public final static String COL_ID = "id"; + public final static String COL_USERNAME = Constants.EXTRAS_USERNAME; + public final static String COL_COOKIE = "cookie"; + public final static String COL_UID = "uid"; + public final static String COL_FULL_NAME = "full_name"; + public final static String COL_PROFILE_PIC = "profile_pic"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + private final int id; + + @ColumnInfo(name = COL_UID) + private final String uid; + + @ColumnInfo(name = COL_USERNAME) + private final String username; + + @ColumnInfo(name = COL_COOKIE) + private final String cookie; + + @ColumnInfo(name = COL_FULL_NAME) + private final String fullName; + + @ColumnInfo(name = COL_PROFILE_PIC) + private final String profilePic; + + @Ignore + private boolean selected; + + public Account(final int id, + final String uid, + final String username, + final String cookie, + final String fullName, + final String profilePic) { + this.id = id; + this.uid = uid; + this.username = username; + this.cookie = cookie; + this.fullName = fullName; + this.profilePic = profilePic; + } + + public int getId() { + return id; + } + + public String getUid() { + return uid; + } + + public String getUsername() { + return username; + } + + public String getCookie() { + return cookie; + } + + public String getFullName() { + return fullName; + } + + public String getProfilePic() { + return profilePic; + } + + public boolean isSelected() { + return selected; + } + + public void setSelected(final boolean selected) { + this.selected = selected; + } + + public boolean isValid() { + return !TextUtils.isEmpty(uid) + && !TextUtils.isEmpty(username) + && !TextUtils.isEmpty(cookie); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Account that = (Account) o; + return ObjectsCompat.equals(uid, that.uid) && + ObjectsCompat.equals(username, that.username) && + ObjectsCompat.equals(cookie, that.cookie); + } + + @Override + public int hashCode() { + return ObjectsCompat.hash(uid, username, cookie); + } + + @NonNull + @Override + public String toString() { + return "Account{" + + "uid='" + uid + '\'' + + ", username='" + username + '\'' + + ", cookie='" + cookie + '\'' + + ", fullName='" + fullName + '\'' + + ", profilePic='" + profilePic + '\'' + + ", selected=" + selected + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/db/entities/Favorite.java b/app/src/main/java/awais/instagrabber/db/entities/Favorite.java new file mode 100644 index 00000000..614ba167 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/entities/Favorite.java @@ -0,0 +1,110 @@ +package awais.instagrabber.db.entities; + +import androidx.annotation.NonNull; +import androidx.core.util.ObjectsCompat; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +import java.util.Date; + +import awais.instagrabber.models.enums.FavoriteType; + +@Entity(tableName = Favorite.TABLE_NAME) +public class Favorite { + public final static String TABLE_NAME = "favorites"; + public final static String COL_ID = "id"; + public final static String COL_QUERY = "query_text"; + public final static String COL_TYPE = "type"; + public final static String COL_DISPLAY_NAME = "display_name"; + public final static String COL_PIC_URL = "pic_url"; + public final static String COL_DATE_ADDED = "date_added"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + private final int id; + + @ColumnInfo(name = COL_QUERY) + private final String query; + + @ColumnInfo(name = COL_TYPE) + private final FavoriteType type; + + @ColumnInfo(name = COL_DISPLAY_NAME) + private final String displayName; + + @ColumnInfo(name = COL_PIC_URL) + private final String picUrl; + + @ColumnInfo(name = COL_DATE_ADDED) + private final Date dateAdded; + + public Favorite(final int id, + final String query, + final FavoriteType type, + final String displayName, + final String picUrl, + final Date dateAdded) { + this.id = id; + this.query = query; + this.type = type; + this.displayName = displayName; + this.picUrl = picUrl; + this.dateAdded = dateAdded; + } + + public int getId() { + return id; + } + + public String getQuery() { + return query; + } + + public FavoriteType getType() { + return type; + } + + public String getDisplayName() { + return displayName; + } + + public String getPicUrl() { + return picUrl; + } + + public Date getDateAdded() { + return dateAdded; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Favorite that = (Favorite) o; + return id == that.id && + ObjectsCompat.equals(query, that.query) && + type == that.type && + ObjectsCompat.equals(displayName, that.displayName) && + ObjectsCompat.equals(picUrl, that.picUrl) && + ObjectsCompat.equals(dateAdded, that.dateAdded); + } + + @Override + public int hashCode() { + return ObjectsCompat.hash(id, query, type, displayName, picUrl, dateAdded); + } + + @NonNull + @Override + public String toString() { + return "FavoriteModel{" + + "id=" + id + + ", query='" + query + '\'' + + ", type=" + type + + ", displayName='" + displayName + '\'' + + ", picUrl='" + picUrl + '\'' + + ", dateAdded=" + dateAdded + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.java b/app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.java new file mode 100644 index 00000000..bb7c48fd --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.java @@ -0,0 +1,131 @@ +package awais.instagrabber.db.repositories; + +import java.util.List; + +import awais.instagrabber.db.datasources.AccountDataSource; +import awais.instagrabber.db.entities.Account; +import awais.instagrabber.utils.AppExecutors; + +public class AccountRepository { + private static final String TAG = AccountRepository.class.getSimpleName(); + + private static AccountRepository instance; + + private final AppExecutors appExecutors; + private final AccountDataSource accountDataSource; + + // private List cachedAccounts; + + private AccountRepository(final AppExecutors appExecutors, final AccountDataSource accountDataSource) { + this.appExecutors = appExecutors; + this.accountDataSource = accountDataSource; + } + + public static AccountRepository getInstance(final AccountDataSource accountDataSource) { + if (instance == null) { + instance = new AccountRepository(AppExecutors.getInstance(), accountDataSource); + } + return instance; + } + + public void getAccount(final String uid, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final Account account = accountDataSource.getAccount(uid); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (account == null) { + callback.onDataNotAvailable(); + return; + } + callback.onSuccess(account); + }); + }); + } + + public void getAllAccounts(final RepositoryCallback> callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final List accounts = accountDataSource.getAllAccounts(); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (accounts == null) { + callback.onDataNotAvailable(); + return; + } + // cachedAccounts = accounts; + callback.onSuccess(accounts); + }); + }); + } + + public void insertOrUpdateAccounts(final List accounts, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + for (final Account account : accounts) { + accountDataSource.insertOrUpdateAccount(account.getUid(), + account.getUsername(), + account.getCookie(), + account.getFullName(), + account.getProfilePic()); + } + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + + public void insertOrUpdateAccount(final String uid, + final String username, + final String cookie, + final String fullName, + final String profilePicUrl, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + accountDataSource.insertOrUpdateAccount(uid, username, cookie, fullName, profilePicUrl); + final Account updated = accountDataSource.getAccount(uid); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (updated == null) { + callback.onDataNotAvailable(); + return; + } + callback.onSuccess(updated); + }); + }); + } + + public void deleteAccount(final Account account, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + accountDataSource.deleteAccount(account); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + + public void deleteAllAccounts(final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + accountDataSource.deleteAllAccounts(); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + +} diff --git a/app/src/main/java/awais/instagrabber/db/repositories/FavoriteRepository.java b/app/src/main/java/awais/instagrabber/db/repositories/FavoriteRepository.java new file mode 100644 index 00000000..926692f7 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/repositories/FavoriteRepository.java @@ -0,0 +1,88 @@ +package awais.instagrabber.db.repositories; + +import java.util.List; + +import awais.instagrabber.db.datasources.FavoriteDataSource; +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.models.enums.FavoriteType; +import awais.instagrabber.utils.AppExecutors; + +public class FavoriteRepository { + private static final String TAG = FavoriteRepository.class.getSimpleName(); + + private static FavoriteRepository instance; + + private final AppExecutors appExecutors; + private final FavoriteDataSource favoriteDataSource; + + private FavoriteRepository(final AppExecutors appExecutors, final FavoriteDataSource favoriteDataSource) { + this.appExecutors = appExecutors; + this.favoriteDataSource = favoriteDataSource; + } + + public static FavoriteRepository getInstance(final FavoriteDataSource favoriteDataSource) { + if (instance == null) { + instance = new FavoriteRepository(AppExecutors.getInstance(), favoriteDataSource); + } + return instance; + } + + public void getFavorite(final String query, final FavoriteType type, final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final Favorite favorite = favoriteDataSource.getFavorite(query, type); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (favorite == null) { + callback.onDataNotAvailable(); + return; + } + callback.onSuccess(favorite); + }); + }); + } + + public void getAllFavorites(final RepositoryCallback> callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final List favorites = favoriteDataSource.getAllFavorites(); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (favorites == null) { + callback.onDataNotAvailable(); + return; + } + callback.onSuccess(favorites); + }); + }); + } + + public void insertOrUpdateFavorite(final Favorite favorite, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + favoriteDataSource.insertOrUpdateFavorite(favorite); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + + public void deleteFavorite(final String query, + final FavoriteType type, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + favoriteDataSource.deleteFavorite(query, type); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } +} diff --git a/app/src/main/java/awais/instagrabber/db/repositories/RepositoryCallback.java b/app/src/main/java/awais/instagrabber/db/repositories/RepositoryCallback.java new file mode 100644 index 00000000..6663239c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/repositories/RepositoryCallback.java @@ -0,0 +1,11 @@ +package awais.instagrabber.db.repositories; + +import androidx.annotation.MainThread; + +public interface RepositoryCallback { + @MainThread + void onSuccess(T result); + + @MainThread + void onDataNotAvailable(); +} diff --git a/app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java index e6de23b3..91afa7d1 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java @@ -21,9 +21,12 @@ import java.util.List; import awais.instagrabber.R; import awais.instagrabber.adapters.AccountSwitcherAdapter; import awais.instagrabber.databinding.DialogAccountSwitcherBinding; +import awais.instagrabber.db.datasources.AccountDataSource; +import awais.instagrabber.db.entities.Account; +import awais.instagrabber.db.repositories.AccountRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DataBox; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; @@ -31,9 +34,20 @@ import static awais.instagrabber.utils.Utils.settingsHelper; public class AccountSwitcherDialogFragment extends DialogFragment { - private final OnAddAccountClickListener onAddAccountClickListener; + private AccountRepository accountRepository; + + private OnAddAccountClickListener onAddAccountClickListener; private DialogAccountSwitcherBinding binding; + public AccountSwitcherDialogFragment() { + accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext())); + } + + public AccountSwitcherDialogFragment(final OnAddAccountClickListener onAddAccountClickListener) { + this.onAddAccountClickListener = onAddAccountClickListener; + accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext())); + } + private final AccountSwitcherAdapter.OnAccountClickListener accountClickListener = (model, isCurrent) -> { if (isCurrent) { dismiss(); @@ -59,8 +73,18 @@ public class AccountSwitcherDialogFragment extends DialogFragment { new AlertDialog.Builder(context) .setMessage(getString(R.string.quick_access_confirm_delete, model.getUsername())) .setPositiveButton(R.string.yes, (dialog, which) -> { - Utils.dataBox.delUserCookie(model); - dismiss(); + if (accountRepository == null) return; + accountRepository.deleteAccount(model, new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + dismiss(); + } + + @Override + public void onDataNotAvailable() { + dismiss(); + } + }); }) .setNegativeButton(R.string.cancel, null) .show(); @@ -68,10 +92,6 @@ public class AccountSwitcherDialogFragment extends DialogFragment { return true; }; - public AccountSwitcherDialogFragment(final OnAddAccountClickListener onAddAccountClickListener) { - this.onAddAccountClickListener = onAddAccountClickListener; - } - @Override public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, @@ -102,11 +122,19 @@ public class AccountSwitcherDialogFragment extends DialogFragment { private void init() { final AccountSwitcherAdapter adapter = new AccountSwitcherAdapter(accountClickListener, accountLongClickListener); binding.accounts.setAdapter(adapter); - final List allUsers = Utils.dataBox.getAllCookies(); - if (allUsers == null) return; - final String cookie = settingsHelper.getString(Constants.COOKIE); - sortUserList(cookie, allUsers); - adapter.submitList(allUsers); + if (accountRepository == null) return; + accountRepository.getAllAccounts(new RepositoryCallback>() { + @Override + public void onSuccess(final List accounts) { + if (accounts == null) return; + final String cookie = settingsHelper.getString(Constants.COOKIE); + sortUserList(cookie, accounts); + adapter.submitList(accounts); + } + + @Override + public void onDataNotAvailable() {} + }); binding.addAccountBtn.setOnClickListener(v -> { if (onAddAccountClickListener == null) return; onAddAccountClickListener.onAddAccountClick(this); @@ -125,9 +153,9 @@ public class AccountSwitcherDialogFragment extends DialogFragment { * @param cookie active cookie * @param allUsers list of users */ - private void sortUserList(final String cookie, final List allUsers) { + private void sortUserList(final String cookie, final List allUsers) { boolean sortByName = true; - for (final DataBox.CookieModel user : allUsers) { + for (final Account user : allUsers) { if (TextUtils.isEmpty(user.getFullName())) { sortByName = false; break; diff --git a/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java index eb0c6a33..f612b72c 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java @@ -19,6 +19,8 @@ import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentTransaction; import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Locale; import awais.instagrabber.databinding.DialogCreateBackupBinding; @@ -32,6 +34,7 @@ import static awais.instagrabber.utils.DownloadUtils.PERMS; public class CreateBackupDialogFragment extends DialogFragment { private static final int STORAGE_PERM_REQUEST_CODE = 8020; + private static final SimpleDateFormat BACKUP_FILE_DATE_TIME_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US); private final OnResultListener onResultListener; private DialogCreateBackupBinding binding; @@ -139,7 +142,8 @@ public class CreateBackupDialogFragment extends DialogFragment { final DirectoryChooser directoryChooser = new DirectoryChooser() .setInitialDirectory(folderPath) .setInteractionListener(path -> { - final File file = new File(path, String.format(Locale.ENGLISH, "barinsta_%d.backup", System.currentTimeMillis())); + final Date now = new Date(); + final File file = new File(path, String.format("barinsta_%s.backup", BACKUP_FILE_DATE_TIME_FORMAT.format(now))); int flags = 0; if (binding.cbExportFavorites.isChecked()) { flags |= ExportImportUtils.FLAG_FAVORITES; @@ -150,12 +154,12 @@ public class CreateBackupDialogFragment extends DialogFragment { if (binding.cbExportLogins.isChecked()) { flags |= ExportImportUtils.FLAG_COOKIES; } - ExportImportUtils.exportData(password, flags, file, result -> { + ExportImportUtils.exportData(context, flags, file, password, result -> { if (onResultListener != null) { onResultListener.onResult(result); } dismiss(); - }, context); + }); }); directoryChooser.setEnterTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); diff --git a/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java index 9b9de1e2..7967b9e2 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java @@ -4,6 +4,7 @@ import android.app.Dialog; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; +import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; @@ -93,7 +94,7 @@ public class RestoreBackupDialogFragment extends DialogFragment { return; } binding.btnRestore.setEnabled(false); - binding.btnRestore.setOnClickListener(v -> { + binding.btnRestore.setOnClickListener(v -> new Handler().post(() -> { int flags = 0; if (binding.cbFavorites.isChecked()) { flags |= ExportImportUtils.FLAG_FAVORITES; @@ -122,7 +123,7 @@ public class RestoreBackupDialogFragment extends DialogFragment { } catch (IncorrectPasswordException e) { binding.passwordField.setError("Incorrect password"); } - }); + })); binding.etPassword.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {} diff --git a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java index 3d344a59..190e9a59 100644 --- a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java @@ -28,9 +28,11 @@ import awais.instagrabber.adapters.FavoritesAdapter; import awais.instagrabber.asyncs.LocationFetcher; import awais.instagrabber.asyncs.ProfileFetcher; import awais.instagrabber.databinding.FragmentFavoritesBinding; -import awais.instagrabber.utils.DataBox; +import awais.instagrabber.db.datasources.FavoriteDataSource; +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.db.repositories.FavoriteRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.FavoritesViewModel; public class FavoritesFragment extends Fragment { @@ -41,6 +43,13 @@ public class FavoritesFragment extends Fragment { private RecyclerView root; private FavoritesViewModel favoritesViewModel; private FavoritesAdapter adapter; + private FavoriteRepository favoriteRepository; + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext())); + } @NonNull @Override @@ -68,9 +77,16 @@ public class FavoritesFragment extends Fragment { if (favoritesViewModel == null || adapter == null) return; // refresh list every time in onViewStateRestored since it is cheaper than implementing pull down to refresh favoritesViewModel.getList().observe(getViewLifecycleOwner(), adapter::submitList); - final List allFavorites = Utils.dataBox.getAllFavorites(); - favoritesViewModel.getList().postValue(allFavorites); - fetchMissingInfo(allFavorites); + favoriteRepository.getAllFavorites(new RepositoryCallback>() { + @Override + public void onSuccess(final List favorites) { + favoritesViewModel.getList().postValue(favorites); + fetchMissingInfo(favorites); + } + + @Override + public void onDataNotAvailable() {} + }); } private void init() { @@ -114,30 +130,43 @@ public class FavoritesFragment extends Fragment { if (context == null) return false; new MaterialAlertDialogBuilder(context) .setMessage(getString(R.string.quick_access_confirm_delete, model.getQuery())) - .setPositiveButton(R.string.yes, (d, which) -> { - Utils.dataBox.deleteFavorite(model.getQuery(), model.getType()); - d.dismiss(); - favoritesViewModel.getList().postValue(Utils.dataBox.getAllFavorites()); - }) + .setPositiveButton(R.string.yes, (d, which) -> favoriteRepository + .deleteFavorite(model.getQuery(), model.getType(), new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + d.dismiss(); + favoriteRepository.getAllFavorites(new RepositoryCallback>() { + @Override + public void onSuccess(final List result) { + favoritesViewModel.getList().postValue(result); + } + + @Override + public void onDataNotAvailable() {} + }); + } + + @Override + public void onDataNotAvailable() {} + })) .setNegativeButton(R.string.no, null) .show(); return true; }); binding.favoriteList.setAdapter(adapter); - // favoritesViewModel.getList().observe(getViewLifecycleOwner(), adapter::submitList); } - private void fetchMissingInfo(final List allFavorites) { + private void fetchMissingInfo(final List allFavorites) { final Runnable runnable = () -> { - final List updatedList = new ArrayList<>(allFavorites); + final List updatedList = new ArrayList<>(allFavorites); // cyclic barrier is to make the async calls synchronous final CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> { // Log.d(TAG, "fetchMissingInfo: barrier action"); favoritesViewModel.getList().postValue(new ArrayList<>(updatedList)); }); try { - for (final DataBox.FavoriteModel model : allFavorites) { + for (final Favorite model : allFavorites) { cyclicBarrier.reset(); // if the model has missing pic or display name (for user and location), fetch those details switch (model.getType()) { @@ -145,27 +174,37 @@ public class FavoritesFragment extends Fragment { if (TextUtils.isEmpty(model.getDisplayName()) || TextUtils.isEmpty(model.getPicUrl())) { new LocationFetcher(model.getQuery(), result -> { - try { - if (result == null) return; - final int i = updatedList.indexOf(model); - updatedList.remove(i); - final DataBox.FavoriteModel updated = new DataBox.FavoriteModel( - model.getId(), - model.getQuery(), - model.getType(), - result.getName(), - result.getSdProfilePic(), - model.getDateAdded() - ); - Utils.dataBox.addOrUpdateFavorite(updated); - updatedList.add(i, updated); - } finally { - try { - cyclicBarrier.await(); - } catch (BrokenBarrierException | InterruptedException e) { - Log.e(TAG, "fetchMissingInfo: ", e); + if (result == null) return; + final int i = updatedList.indexOf(model); + updatedList.remove(i); + final Favorite updated = new Favorite( + model.getId(), + model.getQuery(), + model.getType(), + result.getName(), + result.getSdProfilePic(), + model.getDateAdded() + ); + favoriteRepository.insertOrUpdateFavorite(updated, new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + updatedList.add(i, updated); + try { + cyclicBarrier.await(); + } catch (BrokenBarrierException | InterruptedException e) { + Log.e(TAG, "fetchMissingInfo: ", e); + } } - } + + @Override + public void onDataNotAvailable() { + try { + cyclicBarrier.await(); + } catch (BrokenBarrierException | InterruptedException e) { + Log.e(TAG, "fetchMissingInfo: ", e); + } + } + }); }).execute(); cyclicBarrier.await(); } @@ -174,27 +213,37 @@ public class FavoritesFragment extends Fragment { if (TextUtils.isEmpty(model.getDisplayName()) || TextUtils.isEmpty(model.getPicUrl())) { new ProfileFetcher(model.getQuery(), result -> { - try { - if (result == null) return; - final int i = updatedList.indexOf(model); - updatedList.remove(i); - final DataBox.FavoriteModel updated = new DataBox.FavoriteModel( - model.getId(), - model.getQuery(), - model.getType(), - result.getName(), - result.getSdProfilePic(), - model.getDateAdded() - ); - Utils.dataBox.addOrUpdateFavorite(updated); - updatedList.add(i, updated); - } finally { - try { - cyclicBarrier.await(); - } catch (BrokenBarrierException | InterruptedException e) { - Log.e(TAG, "fetchMissingInfo: ", e); + if (result == null) return; + final int i = updatedList.indexOf(model); + updatedList.remove(i); + final Favorite updated = new Favorite( + model.getId(), + model.getQuery(), + model.getType(), + result.getName(), + result.getSdProfilePic(), + model.getDateAdded() + ); + favoriteRepository.insertOrUpdateFavorite(updated, new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + try { + cyclicBarrier.await(); + } catch (BrokenBarrierException | InterruptedException e) { + Log.e(TAG, "fetchMissingInfo: ", e); + } } - } + + @Override + public void onDataNotAvailable() { + try { + cyclicBarrier.await(); + } catch (BrokenBarrierException | InterruptedException e) { + Log.e(TAG, "fetchMissingInfo: ", e); + } + } + }); + updatedList.add(i, updated); }).execute(); cyclicBarrier.await(); } diff --git a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java index 593e5750..26733e12 100644 --- a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java @@ -49,6 +49,10 @@ import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.customviews.PrimaryActionModeCallback; import awais.instagrabber.databinding.FragmentHashtagBinding; import awais.instagrabber.databinding.LayoutHashtagDetailsBinding; +import awais.instagrabber.db.datasources.FavoriteDataSource; +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.db.repositories.FavoriteRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.HashtagModel; @@ -57,7 +61,6 @@ import awais.instagrabber.models.StoryModel; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DataBox; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; @@ -454,40 +457,60 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe } else { hashtagDetailsBinding.btnFollowTag.setVisibility(View.GONE); } - final DataBox.FavoriteModel favorite = Utils.dataBox.getFavorite(hashtag.substring(1), FavoriteType.HASHTAG); - final boolean isFav = favorite != null; hashtagDetailsBinding.favChip.setVisibility(View.VISIBLE); - hashtagDetailsBinding.favChip.setChipIconResource(isFav ? R.drawable.ic_star_check_24 - : R.drawable.ic_outline_star_plus_24); - hashtagDetailsBinding.favChip.setText(isFav ? R.string.favorite_short : R.string.add_to_favorites); - hashtagDetailsBinding.favChip.setOnClickListener(v -> { - final DataBox.FavoriteModel fav = Utils.dataBox.getFavorite(hashtag.substring(1), FavoriteType.HASHTAG); - final boolean isFavorite = fav != null; - final String message; - if (isFavorite) { - Utils.dataBox.deleteFavorite(hashtag.substring(1), FavoriteType.HASHTAG); - hashtagDetailsBinding.favChip.setText(R.string.add_to_favorites); - hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); - message = getString(R.string.removed_from_favs); - } else { - Utils.dataBox.addOrUpdateFavorite(new DataBox.FavoriteModel( - -1, - hashtag.substring(1), - FavoriteType.HASHTAG, - hashtagModel.getName(), - null, - new Date() - )); - hashtagDetailsBinding.favChip.setText(R.string.favorite_short); + final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext())); + favoriteRepository.getFavorite(hashtag.substring(1), FavoriteType.HASHTAG, new RepositoryCallback() { + @Override + public void onSuccess(final Favorite result) { hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - message = getString(R.string.added_to_favs); + hashtagDetailsBinding.favChip.setText(R.string.favorite_short); + } + + @Override + public void onDataNotAvailable() { + hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + hashtagDetailsBinding.favChip.setText(R.string.add_to_favorites); } - final Snackbar snackbar = Snackbar.make(root, message, BaseTransientBottomBar.LENGTH_LONG); - snackbar.setAction(R.string.ok, v1 -> snackbar.dismiss()) - .setAnimationMode(BaseTransientBottomBar.ANIMATION_MODE_SLIDE) - .setAnchorView(fragmentActivity.getBottomNavView()) - .show(); }); + hashtagDetailsBinding.favChip.setOnClickListener( + v -> favoriteRepository.getFavorite(hashtag.substring(1), FavoriteType.HASHTAG, new RepositoryCallback() { + @Override + public void onSuccess(final Favorite result) { + favoriteRepository.deleteFavorite(hashtag.substring(1), FavoriteType.HASHTAG, new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + hashtagDetailsBinding.favChip.setText(R.string.add_to_favorites); + hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + showSnackbar(getString(R.string.removed_from_favs)); + } + + @Override + public void onDataNotAvailable() {} + }); + } + + @Override + public void onDataNotAvailable() { + favoriteRepository.insertOrUpdateFavorite(new Favorite( + -1, + hashtag.substring(1), + FavoriteType.HASHTAG, + hashtagModel.getName(), + null, + new Date() + ), new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + hashtagDetailsBinding.favChip.setText(R.string.favorite_short); + hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + showSnackbar(getString(R.string.added_to_favs)); + } + + @Override + public void onDataNotAvailable() {} + }); + } + })); hashtagDetailsBinding.mainHashtagImage.setImageURI(hashtagModel.getSdProfilePic()); final String postCount = String.valueOf(hashtagModel.getPostCount()); final SpannableStringBuilder span = new SpannableStringBuilder(getString(R.string.main_posts_count_inline, postCount)); @@ -504,6 +527,14 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe }); } + private void showSnackbar(final String message) { + final Snackbar snackbar = Snackbar.make(root, message, BaseTransientBottomBar.LENGTH_LONG); + snackbar.setAction(R.string.ok, v1 -> snackbar.dismiss()) + .setAnimationMode(BaseTransientBottomBar.ANIMATION_MODE_SLIDE) + .setAnchorView(fragmentActivity.getBottomNavView()) + .show(); + } + private void fetchStories() { if (!isLoggedIn) return; storiesFetching = true; diff --git a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java index c66543fc..acccc0b9 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java @@ -52,6 +52,10 @@ import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.customviews.PrimaryActionModeCallback; import awais.instagrabber.databinding.FragmentLocationBinding; import awais.instagrabber.databinding.LayoutLocationDetailsBinding; +import awais.instagrabber.db.datasources.FavoriteDataSource; +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.db.repositories.FavoriteRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.LocationModel; @@ -60,7 +64,6 @@ import awais.instagrabber.models.StoryModel; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DataBox; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; @@ -443,39 +446,63 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR locationDetailsBinding.locationUrl.setVisibility(View.VISIBLE); locationDetailsBinding.locationUrl.setText(TextUtils.getSpannableUrl(url)); } - final DataBox.FavoriteModel favorite = Utils.dataBox.getFavorite(locationId, FavoriteType.LOCATION); - final boolean isFav = favorite != null; + final FavoriteDataSource dataSource = FavoriteDataSource.getInstance(getContext()); + final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(dataSource); locationDetailsBinding.favChip.setVisibility(View.VISIBLE); - locationDetailsBinding.favChip.setChipIconResource(isFav ? R.drawable.ic_star_check_24 - : R.drawable.ic_outline_star_plus_24); - locationDetailsBinding.favChip.setText(isFav ? R.string.favorite_short : R.string.add_to_favorites); - locationDetailsBinding.favChip.setOnClickListener(v -> { - final DataBox.FavoriteModel fav = Utils.dataBox.getFavorite(locationId, FavoriteType.LOCATION); - final boolean isFavorite = fav != null; - final String message; - if (isFavorite) { - Utils.dataBox.deleteFavorite(locationId, FavoriteType.LOCATION); - locationDetailsBinding.favChip.setText(R.string.add_to_favorites); - locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); - message = getString(R.string.removed_from_favs); - } else { - Utils.dataBox.addOrUpdateFavorite(new DataBox.FavoriteModel( - -1, - locationId, - FavoriteType.LOCATION, - locationModel.getName(), - locationModel.getSdProfilePic(), - new Date() - )); - locationDetailsBinding.favChip.setText(R.string.favorite_short); + favoriteRepository.getFavorite(locationId, FavoriteType.LOCATION, new RepositoryCallback() { + @Override + public void onSuccess(final Favorite result) { locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - message = getString(R.string.added_to_favs); + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + locationDetailsBinding.favChip.setText(R.string.favorite_short); } - final Snackbar snackbar = Snackbar.make(root, message, BaseTransientBottomBar.LENGTH_LONG); - snackbar.setAction(R.string.ok, v1 -> snackbar.dismiss()) - .setAnimationMode(BaseTransientBottomBar.ANIMATION_MODE_SLIDE) - .setAnchorView(fragmentActivity.getBottomNavView()) - .show(); + + @Override + public void onDataNotAvailable() { + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + locationDetailsBinding.favChip.setText(R.string.add_to_favorites); + } + }); + locationDetailsBinding.favChip.setOnClickListener(v -> { + favoriteRepository.getFavorite(locationId, FavoriteType.LOCATION, new RepositoryCallback() { + @Override + public void onSuccess(final Favorite result) { + favoriteRepository.deleteFavorite(locationId, FavoriteType.LOCATION, new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + locationDetailsBinding.favChip.setText(R.string.add_to_favorites); + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + showSnackbar(getString(R.string.removed_from_favs)); + } + + @Override + public void onDataNotAvailable() {} + }); + } + + @Override + public void onDataNotAvailable() { + favoriteRepository.insertOrUpdateFavorite(new Favorite( + -1, + locationId, + FavoriteType.LOCATION, + locationModel.getName(), + locationModel.getSdProfilePic(), + new Date() + ), new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + locationDetailsBinding.favChip.setText(R.string.favorite_short); + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + showSnackbar(getString(R.string.added_to_favs)); + } + + @Override + public void onDataNotAvailable() {} + }); + } + }); }); locationDetailsBinding.mainLocationImage.setOnClickListener(v -> { if (hasStories) { @@ -487,6 +514,14 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR }); } + private void showSnackbar(final String message) { + final Snackbar snackbar = Snackbar.make(root, message, BaseTransientBottomBar.LENGTH_LONG); + snackbar.setAction(R.string.ok, v1 -> snackbar.dismiss()) + .setAnimationMode(BaseTransientBottomBar.ANIMATION_MODE_SLIDE) + .setAnchorView(fragmentActivity.getBottomNavView()) + .show(); + } + private void fetchStories() { if (isLoggedIn) { storiesFetching = true; diff --git a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java index 651bd8b3..f8733382 100644 --- a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java @@ -43,6 +43,8 @@ import awais.instagrabber.webservices.FriendshipService; import awais.instagrabber.webservices.NewsService; import awais.instagrabber.webservices.ServiceCallback; +import static awais.instagrabber.utils.Utils.settingsHelper; + public final class NotificationsViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "NotificationsViewer"; @@ -189,10 +191,12 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe } private void init() { + final Context context = getContext(); + CookieUtils.setupCookies(settingsHelper.getString(Constants.COOKIE)); binding.swipeRefreshLayout.setOnRefreshListener(this); notificationViewModel = new ViewModelProvider(this).get(NotificationViewModel.class); final NotificationsAdapter adapter = new NotificationsAdapter(clickListener, mentionClickListener); - binding.rvComments.setLayoutManager(new LinearLayoutManager(getContext())); + binding.rvComments.setLayoutManager(new LinearLayoutManager(context)); binding.rvComments.setAdapter(adapter); notificationViewModel.getList().observe(getViewLifecycleOwner(), adapter::submitList); onRefresh(); diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 5743bdf7..4c12b4e3 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -63,6 +63,13 @@ import awais.instagrabber.customviews.PrimaryActionModeCallback; import awais.instagrabber.customviews.PrimaryActionModeCallback.CallbacksHelper; import awais.instagrabber.databinding.FragmentProfileBinding; import awais.instagrabber.databinding.LayoutProfileDetailsBinding; +import awais.instagrabber.db.datasources.AccountDataSource; +import awais.instagrabber.db.datasources.FavoriteDataSource; +import awais.instagrabber.db.entities.Account; +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.db.repositories.AccountRepository; +import awais.instagrabber.db.repositories.FavoriteRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; import awais.instagrabber.dialogs.ProfilePicDialogFragment; import awais.instagrabber.fragments.PostViewV2Fragment; @@ -77,7 +84,6 @@ import awais.instagrabber.repositories.responses.FriendshipRepoChangeRootRespons import awais.instagrabber.repositories.responses.FriendshipRepoRestrictRootResponse; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DataBox; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; @@ -283,6 +289,8 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } }; private LayoutProfileDetailsBinding profileDetailsBinding; + private AccountRepository accountRepository; + private FavoriteRepository favoriteRepository; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -290,6 +298,8 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe fragmentActivity = (MainActivity) requireActivity(); friendshipService = FriendshipService.getInstance(); storiesService = StoriesService.getInstance(); + accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext())); + favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext())); setHasOptionsMenu(true); } @@ -498,19 +508,26 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe setUsernameDelayed(); fetchProfileDetails(); }; - boolean found = false; - final DataBox.CookieModel cookieModel = Utils.dataBox.getCookie(uid); - if (cookieModel != null) { - final String username = cookieModel.getUsername(); - if (!TextUtils.isEmpty(username)) { - found = true; - fetchListener.onResult("@" + username); + accountRepository.getAccount(uid, new RepositoryCallback() { + @Override + public void onSuccess(final Account account) { + boolean found = false; + if (account != null) { + final String username = account.getUsername(); + if (!TextUtils.isEmpty(username)) { + found = true; + fetchListener.onResult("@" + username); + } + } + if (!found) { + // if not in database, fetch info from instagram + new UsernameFetcher(uid, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } } - } - if (!found) { - // if not in database, fetch info from instagram - new UsernameFetcher(uid, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } + + @Override + public void onDataNotAvailable() {} + }); return; } fetchProfileDetails(); @@ -556,9 +573,19 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe setupButtons(profileId, myId); if (!profileId.equals(myId)) { profileDetailsBinding.favCb.setVisibility(View.VISIBLE); - final boolean isFav = Utils.dataBox.getFavorite(username.substring(1), FavoriteType.USER) != null; - profileDetailsBinding.favCb.setChecked(isFav); - profileDetailsBinding.favCb.setButtonDrawable(isFav ? R.drawable.ic_star_check_24 : R.drawable.ic_outline_star_plus_24); + favoriteRepository.getFavorite(username.substring(1), FavoriteType.USER, new RepositoryCallback() { + @Override + public void onSuccess(final Favorite result) { + profileDetailsBinding.favCb.setChecked(true); + profileDetailsBinding.favCb.setButtonDrawable(R.drawable.ic_star_check_24); + } + + @Override + public void onDataNotAvailable() { + profileDetailsBinding.favCb.setChecked(false); + profileDetailsBinding.favCb.setButtonDrawable(R.drawable.ic_outline_star_plus_24); + } + }); } else { profileDetailsBinding.favCb.setVisibility(View.GONE); } @@ -842,41 +869,67 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe }); profileDetailsBinding.favCb.setOnCheckedChangeListener((buttonView, isChecked) -> { // do not do anything if state matches the db, as listener is set before profile details are set + final Context context = getContext(); + if (context == null) return; final String finalUsername = username.startsWith("@") ? username.substring(1) : username; - final DataBox.FavoriteModel favorite = Utils.dataBox.getFavorite(finalUsername, FavoriteType.USER); - if ((isChecked && favorite != null) || (!isChecked && favorite == null)) { - return; - } - buttonView.setVisibility(View.GONE); - profileDetailsBinding.favProgress.setVisibility(View.VISIBLE); - final String message; - if (isChecked) { - final DataBox.FavoriteModel model = new DataBox.FavoriteModel( - -1, - finalUsername, - FavoriteType.USER, - profileModel.getName(), - profileModel.getSdProfilePic(), - new Date() - ); - Utils.dataBox.addOrUpdateFavorite(model); - profileDetailsBinding.favCb.setButtonDrawable(R.drawable.ic_star_check_24); - message = getString(R.string.added_to_favs); - } else { - Utils.dataBox.deleteFavorite(finalUsername, FavoriteType.USER); - message = getString(R.string.removed_from_favs); - profileDetailsBinding.favCb.setButtonDrawable(R.drawable.ic_outline_star_plus_24); - } - final Snackbar snackbar = Snackbar.make(root, message, BaseTransientBottomBar.LENGTH_LONG); - snackbar.setAction(R.string.ok, v -> snackbar.dismiss()) - .setAnimationMode(BaseTransientBottomBar.ANIMATION_MODE_SLIDE) - .setAnchorView(fragmentActivity.getBottomNavView()) - .show(); - profileDetailsBinding.favProgress.setVisibility(View.GONE); - profileDetailsBinding.favCb.setVisibility(View.VISIBLE); + favoriteRepository.getFavorite(finalUsername, FavoriteType.USER, new RepositoryCallback() { + @Override + public void onSuccess(final Favorite result) { + if (isChecked) return; // already a fav + buttonView.setVisibility(View.GONE); + profileDetailsBinding.favProgress.setVisibility(View.VISIBLE); + favoriteRepository.deleteFavorite(finalUsername, FavoriteType.USER, new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + profileDetailsBinding.favCb.setButtonDrawable(R.drawable.ic_outline_star_plus_24); + profileDetailsBinding.favProgress.setVisibility(View.GONE); + profileDetailsBinding.favCb.setVisibility(View.VISIBLE); + showSnackbar(getString(R.string.removed_from_favs)); + } + + @Override + public void onDataNotAvailable() {} + }); + } + + @Override + public void onDataNotAvailable() { + if (!isChecked) return; // not in fav already + buttonView.setVisibility(View.GONE); + profileDetailsBinding.favProgress.setVisibility(View.VISIBLE); + final Favorite model = new Favorite( + -1, + finalUsername, + FavoriteType.USER, + profileModel.getName(), + profileModel.getSdProfilePic(), + new Date() + ); + favoriteRepository.insertOrUpdateFavorite(model, new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + profileDetailsBinding.favCb.setButtonDrawable(R.drawable.ic_star_check_24); + profileDetailsBinding.favProgress.setVisibility(View.GONE); + profileDetailsBinding.favCb.setVisibility(View.VISIBLE); + showSnackbar(getString(R.string.added_to_favs)); + } + + @Override + public void onDataNotAvailable() {} + }); + } + }); }); } + private void showSnackbar(final String message) { + final Snackbar snackbar = Snackbar.make(root, message, BaseTransientBottomBar.LENGTH_LONG); + snackbar.setAction(R.string.ok, v -> snackbar.dismiss()) + .setAnimationMode(BaseTransientBottomBar.ANIMATION_MODE_SLIDE) + .setAnchorView(fragmentActivity.getBottomNavView()) + .show(); + } + private void showProfilePicDialog() { if (profileModel != null) { final FragmentManager fragmentManager = getParentFragmentManager(); diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java index 74cb4edd..5dcdde7a 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java @@ -8,6 +8,7 @@ import android.util.Log; import android.view.View; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -26,14 +27,16 @@ import awais.instagrabber.BuildConfig; import awais.instagrabber.R; import awais.instagrabber.activities.Login; import awais.instagrabber.databinding.PrefAccountSwitcherBinding; +import awais.instagrabber.db.datasources.AccountDataSource; +import awais.instagrabber.db.entities.Account; +import awais.instagrabber.db.repositories.AccountRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.dialogs.AccountSwitcherDialogFragment; import awais.instagrabber.repositories.responses.UserInfo; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DataBox; import awais.instagrabber.utils.FlavorTown; import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; import awais.instagrabber.webservices.ProfileService; import awais.instagrabber.webservices.ServiceCallback; @@ -42,20 +45,23 @@ import static awais.instagrabber.utils.Utils.settingsHelper; public class MorePreferencesFragment extends BasePreferencesFragment { private static final String TAG = "MorePreferencesFragment"; + private final AccountRepository accountRepository; + + public MorePreferencesFragment() { + accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext())); + } @Override void setupPreferenceScreen(final PreferenceScreen screen) { final String cookie = settingsHelper.getString(Constants.COOKIE); final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null; // screen.addPreference(new MoreHeaderPreference(getContext())); - final Context context = getContext(); if (context == null) return; final PreferenceCategory accountCategory = new PreferenceCategory(context); accountCategory.setTitle(R.string.account); accountCategory.setIconSpaceReserved(false); screen.addPreference(accountCategory); - final List allCookies = Utils.dataBox.getAllCookies(); if (isLoggedIn) { accountCategory.setSummary(R.string.account_hint); accountCategory.addPreference(getAccountSwitcherPreference(cookie)); @@ -67,34 +73,59 @@ public class MorePreferencesFragment extends BasePreferencesFragment { settingsHelper.putString(Constants.COOKIE, ""); return true; })); - } else { - if (allCookies != null && allCookies.size() > 0) { - accountCategory.addPreference(getAccountSwitcherPreference(null)); - } - // Need to show something to trigger login activity - accountCategory.addPreference(getPreference(R.string.add_account, R.drawable.ic_add, preference -> { - startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE); - return true; - })); } + accountRepository.getAllAccounts(new RepositoryCallback>() { + @Override + public void onSuccess(@NonNull final List accounts) { + if (!isLoggedIn) { + if (accounts.size() > 0) { + accountCategory.addPreference(getAccountSwitcherPreference(null)); + } + // Need to show something to trigger login activity + accountCategory.addPreference(getPreference(R.string.add_account, R.drawable.ic_add, preference -> { + startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE); + return true; + })); + } + if (accounts.size() > 0) { + accountCategory + .addPreference(getPreference(R.string.remove_all_acc, null, R.drawable.ic_account_multiple_remove_24, preference -> { + if (getContext() == null) return false; + new AlertDialog.Builder(getContext()) + .setTitle(R.string.logout) + .setMessage(R.string.remove_all_acc_warning) + .setPositiveButton(R.string.yes, (dialog, which) -> { + CookieUtils.removeAllAccounts(context, new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + shouldRecreate(); + Toast.makeText(context, R.string.logout_success, Toast.LENGTH_SHORT).show(); + settingsHelper.putString(Constants.COOKIE, ""); + } - if (allCookies != null && allCookies.size() > 0) { - accountCategory.addPreference(getPreference(R.string.remove_all_acc, null, R.drawable.ic_account_multiple_remove_24, preference -> { - if (getContext() == null) return false; - new AlertDialog.Builder(getContext()) - .setTitle(R.string.logout) - .setMessage(R.string.remove_all_acc_warning) - .setPositiveButton(R.string.yes, (dialog, which) -> { - CookieUtils.setupCookies("REMOVE"); - shouldRecreate(); - Toast.makeText(context, R.string.logout_success, Toast.LENGTH_SHORT).show(); - settingsHelper.putString(Constants.COOKIE, ""); - }) - .setNegativeButton(R.string.cancel, null) - .show(); - return true; - })); - } + @Override + public void onDataNotAvailable() {} + }); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + return true; + })); + } + } + + @Override + public void onDataNotAvailable() { + Log.d(TAG, "onDataNotAvailable"); + if (!isLoggedIn) { + // Need to show something to trigger login activity + accountCategory.addPreference(getPreference(R.string.add_account, R.drawable.ic_add, preference -> { + startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE); + return true; + })); + } + } + }); // final PreferenceCategory generalCategory = new PreferenceCategory(context); // generalCategory.setTitle(R.string.pref_category_general); @@ -163,11 +194,26 @@ public class MorePreferencesFragment extends BasePreferencesFragment { public void onSuccess(final UserInfo result) { // Log.d(TAG, "adding userInfo: " + result); if (result != null) { - Utils.dataBox.addOrUpdateUser(uid, result.getUsername(), cookie, result.getFullName(), result.getProfilePicUrl()); + accountRepository.insertOrUpdateAccount( + uid, + result.getUsername(), + cookie, + result.getFullName(), + result.getProfilePicUrl(), + new RepositoryCallback() { + @Override + public void onSuccess(final Account result) { + final FragmentActivity activity = getActivity(); + if (activity == null) return; + activity.recreate(); + } + + @Override + public void onDataNotAvailable() { + Log.e(TAG, "onDataNotAvailable: insert failed"); + } + }); } - final FragmentActivity activity = getActivity(); - if (activity == null) return; - activity.recreate(); } @Override @@ -181,7 +227,7 @@ public class MorePreferencesFragment extends BasePreferencesFragment { private AccountSwitcherPreference getAccountSwitcherPreference(final String cookie) { final Context context = getContext(); if (context == null) return null; - return new AccountSwitcherPreference(context, cookie, v -> showAccountSwitcherDialog()); + return new AccountSwitcherPreference(context, cookie, accountRepository, v -> showAccountSwitcherDialog()); } private void showAccountSwitcherDialog() { @@ -244,13 +290,16 @@ public class MorePreferencesFragment extends BasePreferencesFragment { public static class AccountSwitcherPreference extends Preference { private final String cookie; + private final AccountRepository accountRepository; private final View.OnClickListener onClickListener; public AccountSwitcherPreference(final Context context, final String cookie, + final AccountRepository accountRepository, final View.OnClickListener onClickListener) { super(context); this.cookie = cookie; + this.accountRepository = accountRepository; this.onClickListener = onClickListener; setLayoutResource(R.layout.pref_account_switcher); } @@ -263,11 +312,20 @@ public class MorePreferencesFragment extends BasePreferencesFragment { final PrefAccountSwitcherBinding binding = PrefAccountSwitcherBinding.bind(root); final String uid = CookieUtils.getUserIdFromCookie(cookie); if (uid == null) return; - final DataBox.CookieModel user = Utils.dataBox.getCookie(uid); - if (user == null) return; - binding.fullName.setText(user.getFullName()); - binding.username.setText("@" + user.getUsername()); - binding.profilePic.setImageURI(user.getProfilePic()); + accountRepository.getAccount(uid, new RepositoryCallback() { + @Override + public void onSuccess(final Account account) { + binding.getRoot().post(() -> { + binding.fullName.setText(account.getFullName()); + binding.username.setText("@" + account.getUsername()); + binding.profilePic.setImageURI(account.getProfilePic()); + binding.getRoot().requestLayout(); + }); + } + + @Override + public void onDataNotAvailable() {} + }); } } } diff --git a/app/src/main/java/awais/instagrabber/utils/AppExecutors.java b/app/src/main/java/awais/instagrabber/utils/AppExecutors.java new file mode 100644 index 00000000..aaf86807 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/AppExecutors.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package awais.instagrabber.utils; + +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; + +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * Global executor pools for the whole application. + *

+ * Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind + * webservice requests). + */ +public class AppExecutors { + + private static final int THREAD_COUNT = 3; + private static final Object LOCK = new Object(); + + private static AppExecutors instance; + + private final Executor diskIO; + private final Executor networkIO; + private final Executor mainThread; + private final ListeningExecutorService tasksThread; + + private AppExecutors(Executor diskIO, + Executor networkIO, + Executor mainThread, + ListeningExecutorService tasksThread) { + this.diskIO = diskIO; + this.networkIO = networkIO; + this.mainThread = mainThread; + this.tasksThread = tasksThread; + } + + public static AppExecutors getInstance() { + if (instance == null) { + synchronized (LOCK) { + if (instance == null) { + instance = new AppExecutors(Executors.newSingleThreadExecutor(), + Executors.newFixedThreadPool(THREAD_COUNT), + new MainThreadExecutor(), + MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10))); + } + } + } + return instance; + } + + + public Executor diskIO() { + return diskIO; + } + + public Executor networkIO() { + return networkIO; + } + + + public ListeningExecutorService tasksThread() { + return tasksThread; + } + + public Executor mainThread() { + return mainThread; + } + + private static class MainThreadExecutor implements Executor { + private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(@NonNull Runnable command) { + mainThreadHandler.post(command); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/utils/CookieUtils.java b/app/src/main/java/awais/instagrabber/utils/CookieUtils.java index 918dcf38..78a6998e 100644 --- a/app/src/main/java/awais/instagrabber/utils/CookieUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/CookieUtils.java @@ -1,5 +1,6 @@ package awais.instagrabber.utils; +import android.content.Context; import android.util.Log; import android.webkit.CookieManager; @@ -17,9 +18,13 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import awais.instagrabber.BuildConfig; +import awais.instagrabber.db.datasources.AccountDataSource; +import awais.instagrabber.db.repositories.AccountRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; import awaisomereport.LogCollector; public final class CookieUtils { + private static final String TAG = CookieUtils.class.getSimpleName(); public static final CookieManager COOKIE_MANAGER = CookieManager.getInstance(); public static final java.net.CookieManager NET_COOKIE_MANAGER = new java.net.CookieManager(null, CookiePolicy.ACCEPT_ALL); @@ -28,11 +33,7 @@ public final class CookieUtils { if (cookieStore == null || TextUtils.isEmpty(cookieRaw)) { return; } - if (cookieRaw.equals("REMOVE")) { - cookieStore.removeAll(); - Utils.dataBox.deleteAllUserCookies(); - return; - } else if (cookieRaw.equals("LOGOUT")) { + if (cookieRaw.equals("LOGOUT")) { cookieStore.removeAll(); return; } @@ -53,7 +54,19 @@ public final class CookieUtils { } catch (final URISyntaxException e) { if (Utils.logCollector != null) Utils.logCollector.appendException(e, LogCollector.LogFile.UTILS, "setupCookies"); - if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + if (BuildConfig.DEBUG) Log.e(TAG, "", e); + } + } + + public static void removeAllAccounts(final Context context, final RepositoryCallback callback) { + final CookieStore cookieStore = NET_COOKIE_MANAGER.getCookieStore(); + if (cookieStore == null) return; + cookieStore.removeAll(); + try { + AccountRepository.getInstance(AccountDataSource.getInstance(context)) + .deleteAllAccounts(callback); + } catch (Exception e) { + Log.e(TAG, "setupCookies", e); } } diff --git a/app/src/main/java/awais/instagrabber/utils/DataBox.java b/app/src/main/java/awais/instagrabber/utils/DataBox.java index 73e71b04..23f5d889 100755 --- a/app/src/main/java/awais/instagrabber/utils/DataBox.java +++ b/app/src/main/java/awais/instagrabber/utils/DataBox.java @@ -1,26 +1,12 @@ package awais.instagrabber.utils; -import android.content.ContentValues; import android.content.Context; -import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; -import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.util.ObjectsCompat; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import awais.instagrabber.BuildConfig; -import awais.instagrabber.models.enums.FavoriteType; -import awaisomereport.LogCollector; - -import static awais.instagrabber.utils.Utils.logCollector; public final class DataBox extends SQLiteOpenHelper { private static final String TAG = "DataBox"; @@ -28,22 +14,6 @@ public final class DataBox extends SQLiteOpenHelper { private static DataBox sInstance; private final static int VERSION = 3; - private final static String TABLE_COOKIES = "cookies"; - private final static String TABLE_FAVORITES = "favorites"; - - private final static String KEY_ID = "id"; - private final static String KEY_USERNAME = Constants.EXTRAS_USERNAME; - private final static String KEY_COOKIE = "cookie"; - private final static String KEY_UID = "uid"; - private final static String KEY_FULL_NAME = "full_name"; - private final static String KEY_PROFILE_PIC = "profile_pic"; - - private final static String FAV_COL_ID = "id"; - private final static String FAV_COL_QUERY = "query_text"; - private final static String FAV_COL_TYPE = "type"; - private final static String FAV_COL_DISPLAY_NAME = "display_name"; - private final static String FAV_COL_PIC_URL = "pic_url"; - private final static String FAV_COL_DATE_ADDED = "date_added"; public static synchronized DataBox getInstance(final Context context) { if (sInstance == null) sInstance = new DataBox(context.getApplicationContext()); @@ -57,21 +27,6 @@ public final class DataBox extends SQLiteOpenHelper { @Override public void onCreate(@NonNull final SQLiteDatabase db) { Log.i(TAG, "Creating tables..."); - db.execSQL("CREATE TABLE " + TABLE_COOKIES + " (" - + KEY_ID + " INTEGER PRIMARY KEY," - + KEY_UID + " TEXT," - + KEY_USERNAME + " TEXT," - + KEY_COOKIE + " TEXT," - + KEY_FULL_NAME + " TEXT," - + KEY_PROFILE_PIC + " TEXT)"); - // db.execSQL("CREATE TABLE favorites (id INTEGER PRIMARY KEY, query_text TEXT, date_added INTEGER, query_display TEXT)"); - db.execSQL("CREATE TABLE " + TABLE_FAVORITES + " (" - + FAV_COL_ID + " INTEGER PRIMARY KEY," - + FAV_COL_QUERY + " TEXT," - + FAV_COL_TYPE + " TEXT," - + FAV_COL_DISPLAY_NAME + " TEXT," - + FAV_COL_PIC_URL + " TEXT," - + FAV_COL_DATE_ADDED + " INTEGER)"); Log.i(TAG, "Tables created!"); } @@ -81,515 +36,8 @@ public final class DataBox extends SQLiteOpenHelper { // switch without break, so that all migrations from a previous version to new are run switch (oldVersion) { case 1: - db.execSQL("ALTER TABLE " + TABLE_COOKIES + " ADD " + KEY_FULL_NAME + " TEXT"); - db.execSQL("ALTER TABLE " + TABLE_COOKIES + " ADD " + KEY_PROFILE_PIC + " TEXT"); case 2: - final List oldFavorites = backupOldFavorites(db); - // recreate with new columns (as there will be no doubt about the `query_display` column being present or not in the future versions) - db.execSQL("DROP TABLE " + TABLE_FAVORITES); - db.execSQL("CREATE TABLE " + TABLE_FAVORITES + " (" - + FAV_COL_ID + " INTEGER PRIMARY KEY," - + FAV_COL_QUERY + " TEXT," - + FAV_COL_TYPE + " TEXT," - + FAV_COL_DISPLAY_NAME + " TEXT," - + FAV_COL_PIC_URL + " TEXT," - + FAV_COL_DATE_ADDED + " INTEGER)"); - // add the old favorites back - for (final FavoriteModel oldFavorite : oldFavorites) { - addOrUpdateFavorite(db, oldFavorite); - } } Log.i(TAG, String.format("DB update from v%d to v%d completed!", oldVersion, newVersion)); } - - @NonNull - private List backupOldFavorites(@NonNull final SQLiteDatabase db) { - // check if old favorites table had the column query_display - final boolean queryDisplayExists = checkColumnExists(db, TABLE_FAVORITES, "query_display"); - Log.d(TAG, "backupOldFavorites: queryDisplayExists: " + queryDisplayExists); - final List oldModels = new ArrayList<>(); - final String sql = "SELECT " - + "query_text," - + "date_added" - + (queryDisplayExists ? ",query_display" : "") - + " FROM " + TABLE_FAVORITES; - try (final Cursor cursor = db.rawQuery(sql, null)) { - if (cursor != null && cursor.moveToFirst()) { - do { - try { - final String queryText = cursor.getString(cursor.getColumnIndex("query_text")); - final Pair favoriteTypeQueryPair = Utils.migrateOldFavQuery(queryText); - if (favoriteTypeQueryPair == null) continue; - final FavoriteType type = favoriteTypeQueryPair.first; - final String query = favoriteTypeQueryPair.second; - oldModels.add(new FavoriteModel( - -1, - query, - type, - queryDisplayExists ? cursor.getString(cursor.getColumnIndex("query_display")) - : null, - null, - new Date(cursor.getLong(cursor.getColumnIndex("date_added"))) - )); - } catch (Exception e) { - Log.e(TAG, "onUpgrade", e); - } - } while (cursor.moveToNext()); - } - } catch (Exception e) { - Log.e(TAG, "onUpgrade", e); - } - Log.d(TAG, "backupOldFavorites: oldModels:" + oldModels); - return oldModels; - } - - public boolean checkColumnExists(@NonNull final SQLiteDatabase db, - @NonNull final String tableName, - @NonNull final String columnName) { - boolean exists = false; - try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + tableName + ")", null)) { - if (cursor.moveToFirst()) { - do { - final String currentColumn = cursor.getString(cursor.getColumnIndex("name")); - if (currentColumn.equals(columnName)) { - exists = true; - } - } while (cursor.moveToNext()); - - } - } catch (Exception ex) { - Log.e(TAG, "checkColumnExists", ex); - } - return exists; - } - - public final void addOrUpdateFavorite(@NonNull final FavoriteModel model) { - final String query = model.getQuery(); - if (!TextUtils.isEmpty(query)) { - try (final SQLiteDatabase db = getWritableDatabase()) { - db.beginTransaction(); - try { - addOrUpdateFavorite(db, model); - db.setTransactionSuccessful(); - } catch (final Exception e) { - if (logCollector != null) { - logCollector.appendException(e, LogCollector.LogFile.DATA_BOX_FAVORITES, "addOrUpdateFavorite"); - } - if (BuildConfig.DEBUG) { - Log.e(TAG, "Error adding/updating favorite", e); - } - } finally { - db.endTransaction(); - } - } - } - } - - private void addOrUpdateFavorite(@NonNull final SQLiteDatabase db, @NonNull final FavoriteModel model) { - final ContentValues values = new ContentValues(); - values.put(FAV_COL_QUERY, model.getQuery()); - values.put(FAV_COL_TYPE, model.getType().toString()); - values.put(FAV_COL_DISPLAY_NAME, model.getDisplayName()); - values.put(FAV_COL_PIC_URL, model.getPicUrl()); - values.put(FAV_COL_DATE_ADDED, model.getDateAdded().getTime()); - int rows; - if (model.getId() >= 1) { - rows = db.update(TABLE_FAVORITES, values, FAV_COL_ID + "=?", new String[]{String.valueOf(model.getId())}); - } else { - rows = db.update(TABLE_FAVORITES, - values, - FAV_COL_QUERY + "=?" + - " AND " + FAV_COL_TYPE + "=?", - new String[]{model.getQuery(), model.getType().toString()}); - } - if (rows != 1) { - db.insertOrThrow(TABLE_FAVORITES, null, values); - } - } - - public final synchronized void deleteFavorite(@NonNull final String query, @NonNull final FavoriteType type) { - if (!TextUtils.isEmpty(query)) { - try (final SQLiteDatabase db = getWritableDatabase()) { - db.beginTransaction(); - try { - final int rowsDeleted = db.delete(TABLE_FAVORITES, - FAV_COL_QUERY + "=?" + - " AND " + FAV_COL_TYPE + "=?", - new String[]{query, type.toString()}); - - if (rowsDeleted > 0) db.setTransactionSuccessful(); - } catch (final Exception e) { - if (logCollector != null) { - logCollector.appendException(e, LogCollector.LogFile.DATA_BOX_FAVORITES, "deleteFavorite"); - } - if (BuildConfig.DEBUG) { - Log.e(TAG, "Error", e); - } - } finally { - db.endTransaction(); - } - } - } - } - - @NonNull - public final List getAllFavorites() { - final List favorites = new ArrayList<>(); - final SQLiteDatabase db = getWritableDatabase(); - try (final Cursor cursor = db.rawQuery("SELECT " - + FAV_COL_ID + "," - + FAV_COL_QUERY + "," - + FAV_COL_TYPE + "," - + FAV_COL_DISPLAY_NAME + "," - + FAV_COL_PIC_URL + "," - + FAV_COL_DATE_ADDED - + " FROM " + TABLE_FAVORITES, - null)) { - if (cursor != null && cursor.moveToFirst()) { - db.beginTransaction(); - FavoriteModel tempFav; - do { - FavoriteType type = null; - try { - type = FavoriteType.valueOf(cursor.getString(cursor.getColumnIndex(FAV_COL_TYPE))); - } catch (IllegalArgumentException ignored) {} - tempFav = new FavoriteModel( - cursor.getInt(cursor.getColumnIndex(FAV_COL_ID)), - cursor.getString(cursor.getColumnIndex(FAV_COL_QUERY)), - type, - cursor.getString(cursor.getColumnIndex(FAV_COL_DISPLAY_NAME)), - cursor.getString(cursor.getColumnIndex(FAV_COL_PIC_URL)), - new Date(cursor.getLong(cursor.getColumnIndex(FAV_COL_DATE_ADDED))) - ); - favorites.add(tempFav); - } while (cursor.moveToNext()); - db.endTransaction(); - } - } catch (final Exception e) { - Log.e(TAG, "", e); - } - return favorites; - } - - @Nullable - public final FavoriteModel getFavorite(@NonNull final String query, @NonNull final FavoriteType type) { - try (final SQLiteDatabase db = getReadableDatabase(); - final Cursor cursor = db.rawQuery("SELECT " - + FAV_COL_ID + "," - + FAV_COL_QUERY + "," - + FAV_COL_TYPE + "," - + FAV_COL_DISPLAY_NAME + "," - + FAV_COL_PIC_URL + "," - + FAV_COL_DATE_ADDED - + " FROM " + TABLE_FAVORITES - + " WHERE " + FAV_COL_QUERY + "='" + query + "'" - + " AND " + FAV_COL_TYPE + "='" + type.toString() + "'", - null)) { - if (cursor != null && cursor.moveToFirst()) { - FavoriteType favoriteType = null; - try { - favoriteType = FavoriteType.valueOf(cursor.getString(cursor.getColumnIndex(FAV_COL_TYPE))); - } catch (IllegalArgumentException ignored) {} - return new FavoriteModel( - cursor.getInt(cursor.getColumnIndex(FAV_COL_ID)), - cursor.getString(cursor.getColumnIndex(FAV_COL_QUERY)), - favoriteType, - cursor.getString(cursor.getColumnIndex(FAV_COL_DISPLAY_NAME)), - cursor.getString(cursor.getColumnIndex(FAV_COL_PIC_URL)), - new Date(cursor.getLong(cursor.getColumnIndex(FAV_COL_DATE_ADDED))) - ); - } - } - return null; - } - - public final void addOrUpdateUser(@NonNull final DataBox.CookieModel cookieModel) { - addOrUpdateUser( - cookieModel.getUid(), - cookieModel.getUsername(), - cookieModel.getCookie(), - cookieModel.getFullName(), - cookieModel.getProfilePic() - ); - } - - public final void addOrUpdateUser(final String uid, - final String username, - final String cookie, - final String fullName, - final String profilePicUrl) { - if (TextUtils.isEmpty(uid)) return; - try (final SQLiteDatabase db = getWritableDatabase()) { - db.beginTransaction(); - try { - final ContentValues values = new ContentValues(); - values.put(KEY_USERNAME, username); - values.put(KEY_COOKIE, cookie); - values.put(KEY_UID, uid); - values.put(KEY_FULL_NAME, fullName); - values.put(KEY_PROFILE_PIC, profilePicUrl); - - final int rows = db.update(TABLE_COOKIES, values, KEY_UID + "=?", new String[]{uid}); - - if (rows != 1) - db.insertOrThrow(TABLE_COOKIES, null, values); - - db.setTransactionSuccessful(); - } catch (final Exception e) { - if (BuildConfig.DEBUG) Log.e(TAG, "Error", e); - } finally { - db.endTransaction(); - } - } - } - - public final synchronized void delUserCookie(@NonNull final CookieModel cookieModel) { - final String cookieModelUid = cookieModel.getUid(); - if (!TextUtils.isEmpty(cookieModelUid)) { - try (final SQLiteDatabase db = getWritableDatabase()) { - db.beginTransaction(); - try { - final int rowsDeleted = db.delete(TABLE_COOKIES, KEY_UID + "=? AND " + KEY_USERNAME + "=? AND " + KEY_COOKIE + "=?", - new String[]{cookieModelUid, cookieModel.getUsername(), cookieModel.getCookie()}); - - if (rowsDeleted > 0) db.setTransactionSuccessful(); - } catch (final Exception e) { - if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); - } finally { - db.endTransaction(); - } - } - } - } - - public final synchronized void deleteAllUserCookies() { - try (final SQLiteDatabase db = getWritableDatabase()) { - db.beginTransaction(); - try { - final int rowsDeleted = db.delete(TABLE_COOKIES, null, null); - - if (rowsDeleted > 0) db.setTransactionSuccessful(); - } catch (final Exception e) { - if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); - } finally { - db.endTransaction(); - } - } - } - - @Nullable - public final CookieModel getCookie(final String uid) { - CookieModel cookie = null; - try (final SQLiteDatabase db = getReadableDatabase(); - final Cursor cursor = db.rawQuery( - "SELECT " - + KEY_UID + "," - + KEY_USERNAME + "," - + KEY_COOKIE + "," - + KEY_FULL_NAME + "," - + KEY_PROFILE_PIC - + " FROM " + TABLE_COOKIES - + " WHERE " + KEY_UID + " = ?", - new String[]{uid}) - ) { - if (cursor != null && cursor.moveToFirst()) - cookie = new CookieModel( - cursor.getString(cursor.getColumnIndex(KEY_UID)), - cursor.getString(cursor.getColumnIndex(KEY_USERNAME)), - cursor.getString(cursor.getColumnIndex(KEY_COOKIE)), - cursor.getString(cursor.getColumnIndex(KEY_FULL_NAME)), - cursor.getString(cursor.getColumnIndex(KEY_PROFILE_PIC)) - ); - } - return cookie; - } - - @NonNull - public final List getAllCookies() { - final List cookies = new ArrayList<>(); - try (final SQLiteDatabase db = getReadableDatabase(); - final Cursor cursor = db.rawQuery( - "SELECT " - + KEY_UID + "," - + KEY_USERNAME + "," - + KEY_COOKIE + "," - + KEY_FULL_NAME + "," - + KEY_PROFILE_PIC - + " FROM " + TABLE_COOKIES, null) - ) { - if (cursor != null && cursor.moveToFirst()) { - do { - cookies.add(new CookieModel( - cursor.getString(cursor.getColumnIndex(KEY_UID)), - cursor.getString(cursor.getColumnIndex(KEY_USERNAME)), - cursor.getString(cursor.getColumnIndex(KEY_COOKIE)), - cursor.getString(cursor.getColumnIndex(KEY_FULL_NAME)), - cursor.getString(cursor.getColumnIndex(KEY_PROFILE_PIC)) - )); - } while (cursor.moveToNext()); - } - } - return cookies; - } - - public static class CookieModel { - private final String uid; - private final String username; - private final String cookie; - private final String fullName; - private final String profilePic; - private boolean selected; - - public CookieModel(final String uid, - final String username, - final String cookie, - final String fullName, - final String profilePic) { - this.uid = uid; - this.username = username; - this.cookie = cookie; - this.fullName = fullName; - this.profilePic = profilePic; - } - - public String getUid() { - return uid; - } - - public String getUsername() { - return username; - } - - public String getCookie() { - return cookie; - } - - public String getFullName() { - return fullName; - } - - public String getProfilePic() { - return profilePic; - } - - public boolean isSelected() { - return selected; - } - - public void setSelected(final boolean selected) { - this.selected = selected; - } - - public boolean isValid() { - return !TextUtils.isEmpty(uid) - && !TextUtils.isEmpty(username) - && !TextUtils.isEmpty(cookie); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final CookieModel that = (CookieModel) o; - return ObjectsCompat.equals(uid, that.uid) && - ObjectsCompat.equals(username, that.username) && - ObjectsCompat.equals(cookie, that.cookie); - } - - @Override - public int hashCode() { - return ObjectsCompat.hash(uid, username, cookie); - } - - @NonNull - @Override - public String toString() { - return "CookieModel{" + - "uid='" + uid + '\'' + - ", username='" + username + '\'' + - ", cookie='" + cookie + '\'' + - ", fullName='" + fullName + '\'' + - ", profilePic='" + profilePic + '\'' + - ", selected=" + selected + - '}'; - } - } - - public static class FavoriteModel { - private final int id; - private final String query; - private final FavoriteType type; - private final String displayName; - private final String picUrl; - private final Date dateAdded; - - public FavoriteModel(final int id, - final String query, - final FavoriteType type, - final String displayName, - final String picUrl, - final Date dateAdded) { - this.id = id; - this.query = query; - this.type = type; - this.displayName = displayName; - this.picUrl = picUrl; - this.dateAdded = dateAdded; - } - - public int getId() { - return id; - } - - public String getQuery() { - return query; - } - - public FavoriteType getType() { - return type; - } - - public String getDisplayName() { - return displayName; - } - - public String getPicUrl() { - return picUrl; - } - - public Date getDateAdded() { - return dateAdded; - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final FavoriteModel that = (FavoriteModel) o; - return id == that.id && - ObjectsCompat.equals(query, that.query) && - type == that.type && - ObjectsCompat.equals(displayName, that.displayName) && - ObjectsCompat.equals(picUrl, that.picUrl) && - ObjectsCompat.equals(dateAdded, that.dateAdded); - } - - @Override - public int hashCode() { - return ObjectsCompat.hash(id, query, type, displayName, picUrl, dateAdded); - } - - @NonNull - @Override - public String toString() { - return "FavoriteModel{" + - "id=" + id + - ", query='" + query + '\'' + - ", type=" + type + - ", displayName='" + displayName + '\'' + - ", picUrl='" + picUrl + '\'' + - ", dateAdded=" + dateAdded + - '}'; - } - } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java index 3770bd65..dab09b03 100755 --- a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java @@ -9,8 +9,14 @@ import android.widget.Toast; import androidx.annotation.IntDef; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import org.checkerframework.checker.nullness.compatqual.NullableDecl; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -18,12 +24,20 @@ import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import awais.instagrabber.BuildConfig; +import awais.instagrabber.db.datasources.AccountDataSource; +import awais.instagrabber.db.datasources.FavoriteDataSource; +import awais.instagrabber.db.entities.Account; +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.db.repositories.AccountRepository; +import awais.instagrabber.db.repositories.FavoriteRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException; @@ -39,47 +53,6 @@ public final class ExportImportUtils { public static final int FLAG_FAVORITES = 1 << 1; public static final int FLAG_SETTINGS = 1 << 2; - @IntDef(value = {FLAG_COOKIES, FLAG_FAVORITES, FLAG_SETTINGS}, flag = true) - @interface ExportImportFlags {} - - public static void exportData(@Nullable final String password, - @ExportImportFlags final int flags, - @NonNull final File filePath, - final FetchListener fetchListener, - @NonNull final Context context) { - final String exportString = getExportString(flags, context); - if (TextUtils.isEmpty(exportString)) return; - final boolean isPass = !TextUtils.isEmpty(password); - byte[] exportBytes = null; - if (isPass) { - final byte[] passwordBytes = password.getBytes(); - final byte[] bytes = new byte[32]; - System.arraycopy(passwordBytes, 0, bytes, 0, Math.min(passwordBytes.length, 32)); - try { - exportBytes = PasswordUtils.enc(exportString, bytes); - } catch (final Exception e) { - if (fetchListener != null) fetchListener.onResult(false); - if (logCollector != null) - logCollector.appendException(e, LogFile.UTILS_EXPORT, "Export::isPass"); - if (BuildConfig.DEBUG) Log.e(TAG, "", e); - } - } else { - exportBytes = Base64.encode(exportString.getBytes(), Base64.DEFAULT | Base64.NO_WRAP | Base64.NO_PADDING); - } - if (exportBytes != null && exportBytes.length > 1) { - try (final FileOutputStream fos = new FileOutputStream(filePath)) { - fos.write(isPass ? 'A' : 'Z'); - fos.write(exportBytes); - if (fetchListener != null) fetchListener.onResult(true); - } catch (final Exception e) { - if (fetchListener != null) fetchListener.onResult(false); - if (logCollector != null) - logCollector.appendException(e, LogFile.UTILS_EXPORT, "Export::notPass"); - if (BuildConfig.DEBUG) Log.e(TAG, "", e); - } - } else if (fetchListener != null) fetchListener.onResult(false); - } - public static void importData(@NonNull final Context context, @ExportImportFlags final int flags, @NonNull final File file, @@ -99,7 +72,8 @@ public final class ExportImportUtils { final byte[] passwordBytes = password.getBytes(); final byte[] bytes = new byte[32]; System.arraycopy(passwordBytes, 0, bytes, 0, Math.min(passwordBytes.length, 32)); - importJson(new String(PasswordUtils.dec(builder.toString(), bytes)), + importJson(context, + new String(PasswordUtils.dec(builder.toString(), bytes)), flags, fetchListener); } catch (final IncorrectPasswordException e) { @@ -111,7 +85,8 @@ public final class ExportImportUtils { if (BuildConfig.DEBUG) Log.e(TAG, "Error importing backup", e); } } else if (configType == 'Z') { - importJson(new String(Base64.decode(builder.toString(), Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP)), + importJson(context, + new String(Base64.decode(builder.toString(), Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP)), flags, fetchListener); @@ -129,7 +104,8 @@ public final class ExportImportUtils { } } - private static void importJson(@NonNull final String json, + private static void importJson(final Context context, + @NonNull final String json, @ExportImportFlags final int flags, final FetchListener fetchListener) { try { @@ -138,10 +114,10 @@ public final class ExportImportUtils { importSettings(jsonObject); } if ((flags & FLAG_COOKIES) == FLAG_COOKIES && jsonObject.has("cookies")) { - importAccounts(jsonObject); + importAccounts(context, jsonObject); } if ((flags & FLAG_FAVORITES) == FLAG_FAVORITES && jsonObject.has("favs")) { - importFavorites(jsonObject); + importFavorites(context, jsonObject); } if (fetchListener != null) fetchListener.onResult(true); } catch (final Exception e) { @@ -151,7 +127,7 @@ public final class ExportImportUtils { } } - private static void importFavorites(final JSONObject jsonObject) throws JSONException { + private static void importFavorites(final Context context, final JSONObject jsonObject) throws JSONException { final JSONArray favs = jsonObject.getJSONArray("favs"); for (int i = 0; i < favs.length(); i++) { final JSONObject favsObject = favs.getJSONObject(i); @@ -175,7 +151,7 @@ public final class ExportImportUtils { if (query == null || favoriteType == null) { continue; } - final DataBox.FavoriteModel favoriteModel = new DataBox.FavoriteModel( + final Favorite favorite = new Favorite( -1, query, favoriteType, @@ -184,41 +160,55 @@ public final class ExportImportUtils { : favsObject.optString("pic_url"), new Date(favsObject.getLong("d"))); // Log.d(TAG, "importJson: favoriteModel: " + favoriteModel); - Utils.dataBox.addOrUpdateFavorite(favoriteModel); + FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context)) + .insertOrUpdateFavorite(favorite, null); } } - private static void importAccounts(final JSONObject jsonObject) throws JSONException { - final JSONArray cookies = jsonObject.getJSONArray("cookies"); - for (int i = 0; i < cookies.length(); i++) { - final JSONObject cookieObject = cookies.getJSONObject(i); - final DataBox.CookieModel cookieModel = new DataBox.CookieModel( - cookieObject.optString("i"), - cookieObject.optString("u"), - cookieObject.optString("c"), - cookieObject.optString("full_name"), - cookieObject.optString("profile_pic") - ); - if (!cookieModel.isValid()) continue; - // Log.d(TAG, "importJson: cookieModel: " + cookieModel); - Utils.dataBox.addOrUpdateUser(cookieModel); - } - } - - private static void importSettings(final JSONObject jsonObject) throws JSONException { - final JSONObject objSettings = jsonObject.getJSONObject("settings"); - final Iterator keys = objSettings.keys(); - while (keys.hasNext()) { - final String key = keys.next(); - final Object val = objSettings.opt(key); - // Log.d(TAG, "importJson: key: " + key + ", val: " + val); - if (val instanceof String) { - settingsHelper.putString(key, (String) val); - } else if (val instanceof Integer) { - settingsHelper.putInteger(key, (int) val); - } else if (val instanceof Boolean) { - settingsHelper.putBoolean(key, (boolean) val); + private static void importAccounts(final Context context, + final JSONObject jsonObject) { + final List accounts = new ArrayList<>(); + try { + final JSONArray cookies = jsonObject.getJSONArray("cookies"); + for (int i = 0; i < cookies.length(); i++) { + final JSONObject cookieObject = cookies.getJSONObject(i); + final Account account = new Account( + -1, + cookieObject.optString("i"), + cookieObject.optString("u"), + cookieObject.optString("c"), + cookieObject.optString("full_name"), + cookieObject.optString("profile_pic") + ); + if (!account.isValid()) continue; + accounts.add(account); } + } catch (Exception e) { + Log.e(TAG, "importAccounts: Error parsing json", e); + return; + } + AccountRepository.getInstance(AccountDataSource.getInstance(context)) + .insertOrUpdateAccounts(accounts, null); + } + + private static void importSettings(final JSONObject jsonObject) { + try { + final JSONObject objSettings = jsonObject.getJSONObject("settings"); + final Iterator keys = objSettings.keys(); + while (keys.hasNext()) { + final String key = keys.next(); + final Object val = objSettings.opt(key); + // Log.d(TAG, "importJson: key: " + key + ", val: " + val); + if (val instanceof String) { + settingsHelper.putString(key, (String) val); + } else if (val instanceof Integer) { + settingsHelper.putInteger(key, (int) val); + } else if (val instanceof Boolean) { + settingsHelper.putBoolean(key, (boolean) val); + } + } + } catch (Exception e) { + Log.e(TAG, "importSettings error", e); } } @@ -234,97 +224,209 @@ public final class ExportImportUtils { return false; } - @Nullable - private static String getExportString(@ExportImportFlags final int flags, - @NonNull final Context context) { - String result = null; - try { - final JSONObject jsonObject = new JSONObject(); - if ((flags & FLAG_SETTINGS) == FLAG_SETTINGS) { - jsonObject.put("settings", getSettings(context)); - } - if ((flags & FLAG_COOKIES) == FLAG_COOKIES) { - jsonObject.put("cookies", getCookies()); - } - if ((flags & FLAG_FAVORITES) == FLAG_FAVORITES) { - jsonObject.put("favs", getFavorites()); + public static void exportData(@NonNull final Context context, + @ExportImportFlags final int flags, + @NonNull final File filePath, + final String password, + final FetchListener fetchListener) { + getExportString(flags, context, exportString -> { + if (TextUtils.isEmpty(exportString)) return; + final boolean isPass = !TextUtils.isEmpty(password); + byte[] exportBytes = null; + if (isPass) { + final byte[] passwordBytes = password.getBytes(); + final byte[] bytes = new byte[32]; + System.arraycopy(passwordBytes, 0, bytes, 0, Math.min(passwordBytes.length, 32)); + try { + exportBytes = PasswordUtils.enc(exportString, bytes); + } catch (final Exception e) { + if (fetchListener != null) fetchListener.onResult(false); + if (logCollector != null) + logCollector.appendException(e, LogFile.UTILS_EXPORT, "Export::isPass"); + if (BuildConfig.DEBUG) Log.e(TAG, "", e); + } + } else { + exportBytes = Base64.encode(exportString.getBytes(), Base64.DEFAULT | Base64.NO_WRAP | Base64.NO_PADDING); } + if (exportBytes != null && exportBytes.length > 1) { + try (final FileOutputStream fos = new FileOutputStream(filePath)) { + fos.write(isPass ? 'A' : 'Z'); + fos.write(exportBytes); + if (fetchListener != null) fetchListener.onResult(true); + } catch (final Exception e) { + if (fetchListener != null) fetchListener.onResult(false); + if (logCollector != null) + logCollector.appendException(e, LogFile.UTILS_EXPORT, "Export::notPass"); + if (BuildConfig.DEBUG) Log.e(TAG, "", e); + } + } else if (fetchListener != null) fetchListener.onResult(false); + }); - result = jsonObject.toString(); + } + + private static void getExportString(@ExportImportFlags final int flags, + @NonNull final Context context, + final OnExportStringCreatedCallback callback) { + if (callback == null) return; + try { + final ImmutableList.Builder> futures = ImmutableList.builder(); + futures.add((flags & FLAG_SETTINGS) == FLAG_SETTINGS + ? getSettings(context) + : Futures.immediateFuture(null)); + futures.add((flags & FLAG_COOKIES) == FLAG_COOKIES + ? getCookies(context) + : Futures.immediateFuture(null)); + futures.add((flags & FLAG_FAVORITES) == FLAG_FAVORITES + ? getFavorites(context) + : Futures.immediateFuture(null)); + //noinspection UnstableApiUsage + final ListenableFuture> allFutures = Futures.allAsList(futures.build()); + Futures.addCallback(allFutures, new FutureCallback>() { + @Override + public void onSuccess(@NullableDecl final List result) { + final JSONObject jsonObject = new JSONObject(); + if (result == null) { + callback.onCreated(jsonObject.toString()); + return; + } + try { + final JSONObject settings = (JSONObject) result.get(0); + if (settings != null) { + jsonObject.put("settings", settings); + } + } catch (Exception e) { + Log.e(TAG, "error getting settings: ", e); + } + try { + final JSONArray accounts = (JSONArray) result.get(1); + if (accounts != null) { + jsonObject.put("cookies", accounts); + } + } catch (Exception e) { + Log.e(TAG, "error getting accounts", e); + } + try { + final JSONArray favorites = (JSONArray) result.get(2); + if (favorites != null) { + jsonObject.put("favs", favorites); + } + } catch (Exception e) { + Log.e(TAG, "error getting favorites: ", e); + } + callback.onCreated(jsonObject.toString()); + } + + @Override + public void onFailure(@NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + callback.onCreated(null); + } + }, AppExecutors.getInstance().tasksThread()); + return; } catch (final Exception e) { if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_EXPORT, "getExportString"); if (BuildConfig.DEBUG) Log.e(TAG, "", e); } - return result; + callback.onCreated(null); } @NonNull - private static JSONObject getSettings(@NonNull final Context context) { + private static ListenableFuture getSettings(@NonNull final Context context) { final SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); - final Map allPrefs = sharedPreferences.getAll(); - if (allPrefs == null) { + return AppExecutors.getInstance().tasksThread().submit(() -> { + final Map allPrefs = sharedPreferences.getAll(); + if (allPrefs == null) { + return new JSONObject(); + } + try { + final JSONObject jsonObject = new JSONObject(allPrefs); + jsonObject.remove(Constants.COOKIE); + jsonObject.remove(Constants.DEVICE_UUID); + jsonObject.remove(Constants.PREV_INSTALL_VERSION); + return jsonObject; + } catch (Exception e) { + Log.e(TAG, "Error exporting settings", e); + } return new JSONObject(); - } - try { - final JSONObject jsonObject = new JSONObject(allPrefs); - jsonObject.remove(Constants.COOKIE); - jsonObject.remove(Constants.DEVICE_UUID); - jsonObject.remove(Constants.PREV_INSTALL_VERSION); - return jsonObject; - } catch (Exception e) { - Log.e(TAG, "Error exporting settings", e); - } - return new JSONObject(); + }); } - @NonNull - private static JSONArray getFavorites() { - if (Utils.dataBox == null) return new JSONArray(); - try { - final List allFavorites = Utils.dataBox.getAllFavorites(); - final JSONArray jsonArray = new JSONArray(); - for (final DataBox.FavoriteModel favorite : allFavorites) { - final JSONObject jsonObject = new JSONObject(); - jsonObject.put("q", favorite.getQuery()); - jsonObject.put("type", favorite.getType().toString()); - jsonObject.put("s", favorite.getDisplayName()); - jsonObject.put("pic_url", favorite.getPicUrl()); - jsonObject.put("d", favorite.getDateAdded().getTime()); - jsonArray.put(jsonObject); + private static ListenableFuture getFavorites(final Context context) { + final SettableFuture future = SettableFuture.create(); + final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context)); + favoriteRepository.getAllFavorites(new RepositoryCallback>() { + @Override + public void onSuccess(final List favorites) { + final JSONArray jsonArray = new JSONArray(); + try { + for (final Favorite favorite : favorites) { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("q", favorite.getQuery()); + jsonObject.put("type", favorite.getType().toString()); + jsonObject.put("s", favorite.getDisplayName()); + jsonObject.put("pic_url", favorite.getPicUrl()); + jsonObject.put("d", favorite.getDateAdded().getTime()); + jsonArray.put(jsonObject); + } + } catch (Exception e) { + if (logCollector != null) { + logCollector.appendException(e, LogFile.UTILS_EXPORT, "getFavorites"); + } + if (BuildConfig.DEBUG) { + Log.e(TAG, "Error exporting favorites", e); + } + } + future.set(jsonArray); } - return jsonArray; - } catch (final Exception e) { - if (logCollector != null) { - logCollector.appendException(e, LogFile.UTILS_EXPORT, "getFavorites"); + + @Override + public void onDataNotAvailable() { + future.set(new JSONArray()); } - if (BuildConfig.DEBUG) { - Log.e(TAG, "Error exporting favorites", e); - } - } - return new JSONArray(); + }); + return future; } - @NonNull - private static JSONArray getCookies() { - if (Utils.dataBox == null) return new JSONArray(); - try { - final List allCookies = Utils.dataBox.getAllCookies(); - final JSONArray jsonArray = new JSONArray(); - for (final DataBox.CookieModel cookie : allCookies) { - final JSONObject jsonObject = new JSONObject(); - jsonObject.put("i", cookie.getUid()); - jsonObject.put("u", cookie.getUsername()); - jsonObject.put("c", cookie.getCookie()); - jsonObject.put("full_name", cookie.getFullName()); - jsonObject.put("profile_pic", cookie.getProfilePic()); - jsonArray.put(jsonObject); + private static ListenableFuture getCookies(final Context context) { + final SettableFuture future = SettableFuture.create(); + final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context)); + accountRepository.getAllAccounts(new RepositoryCallback>() { + @Override + public void onSuccess(final List accounts) { + final JSONArray jsonArray = new JSONArray(); + try { + for (final Account cookie : accounts) { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("i", cookie.getUid()); + jsonObject.put("u", cookie.getUsername()); + jsonObject.put("c", cookie.getCookie()); + jsonObject.put("full_name", cookie.getFullName()); + jsonObject.put("profile_pic", cookie.getProfilePic()); + jsonArray.put(jsonObject); + } + } catch (Exception e) { + if (logCollector != null) { + logCollector.appendException(e, LogFile.UTILS_EXPORT, "getCookies"); + } + if (BuildConfig.DEBUG) { + Log.e(TAG, "Error exporting accounts", e); + } + } + future.set(jsonArray); } - return jsonArray; - } catch (final Exception e) { - if (BuildConfig.DEBUG) { - Log.e(TAG, "Error exporting accounts", e); + + @Override + public void onDataNotAvailable() { + future.set(new JSONArray()); } - } - return new JSONArray(); + }); + return future; + } + + @IntDef(value = {FLAG_COOKIES, FLAG_FAVORITES, FLAG_SETTINGS}, flag = true) + @interface ExportImportFlags {} + + public interface OnExportStringCreatedCallback { + void onCreated(String exportString); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index 04cba7fc..786475f9 100644 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -47,7 +47,6 @@ public final class Utils { public static LogCollector logCollector; public static SettingsHelper settingsHelper; - public static DataBox dataBox; public static boolean sessionVolumeFull = false; public static final MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); public static ClipboardManager clipboardManager; diff --git a/app/src/main/java/awais/instagrabber/viewmodels/FavoritesViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/FavoritesViewModel.java index e30d4116..bb865b88 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/FavoritesViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/FavoritesViewModel.java @@ -5,12 +5,12 @@ import androidx.lifecycle.ViewModel; import java.util.List; -import awais.instagrabber.utils.DataBox; +import awais.instagrabber.db.entities.Favorite; public class FavoritesViewModel extends ViewModel { - private MutableLiveData> list; + private MutableLiveData> list; - public MutableLiveData> getList() { + public MutableLiveData> getList() { if (list == null) { list = new MutableLiveData<>(); } diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index ff4a275d..ec72e8a1 100755 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -22,7 +22,7 @@ android:id="@+id/cookies" style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog" android:layout_width="0dp" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/get_cookies" /> @@ -30,14 +30,15 @@ android:id="@+id/refresh" style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog" android:layout_width="0dp" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/refresh" /> diff --git a/app/src/main/res/layout/layout_profile_details.xml b/app/src/main/res/layout/layout_profile_details.xml index 0c46eab4..41abd3b3 100644 --- a/app/src/main/res/layout/layout_profile_details.xml +++ b/app/src/main/res/layout/layout_profile_details.xml @@ -101,7 +101,7 @@