@@ -121,7 +125,7 @@ This app's predecessor, InstaGrabber, was originally made by [@AwaisKing](https:
Barinsta
Copyright (C) 2020-2021 Austin Huang
- Ammar Githam
+ Ammar Githam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
diff --git a/app/build.gradle b/app/build.gradle
index 871170bf..b85aecad 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,5 +1,7 @@
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
apply plugin: "androidx.navigation.safeargs"
+apply plugin: 'kotlin-kapt'
apply from: 'sentry.gradle'
def getGitHash = { ->
@@ -20,8 +22,8 @@ android {
minSdkVersion 21
targetSdkVersion 30
- versionCode 61
- versionName '19.2.0'
+ versionCode 63
+ versionName '19.2.2'
multiDexEnabled true
@@ -33,6 +35,12 @@ android {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ sourceSets {
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
compileOptions {
@@ -76,6 +84,27 @@ android {
}
}
+ splits {
+ // Configures multiple APKs based on ABI.
+ abi {
+ // Enables building multiple APKs per ABI.
+ enable project.hasProperty("split") && !gradle.startParameter.taskNames.isEmpty() && gradle.startParameter.taskNames.get(0).contains('Release')
+
+ // By default all ABIs are included, so use reset() and include to specify that we only
+ // want APKs for x86 and x86_64.
+
+ // Resets the list of ABIs that Gradle should create APKs for to none.
+ reset()
+
+ // Specifies a list of ABIs that Gradle should create APKs for.
+ include "x86", "x86_64", "arm64-v8a", "armeabi-v7a"
+
+ // Specifies that we want to also generate a universal APK that includes all ABIs.
+ universalApk true
+ }
+ }
+
+
android.applicationVariants.all { variant ->
if (variant.flavorName != "github") return
variant.outputs.all { output ->
@@ -84,17 +113,47 @@ android {
// def versionCode = variant.versionCode
def flavor = variant.flavorName
- def suffix = "${versionName}-${flavor}_${builtType}" // eg. 19.1.0-github_debug or release
+ def flavorBuiltType = "${flavor}_${builtType}"
+ def suffix
+ // For x86 and x86_64, the versionNames are already overridden
+ if (versionName.contains(flavorBuiltType)) {
+ suffix = "${versionName}"
+ } else {
+ suffix = "${versionName}-${flavorBuiltType}" // eg. 19.1.0-github_debug or release
+ }
if (builtType.toString() == 'release' && project.hasProperty("pre")) {
buildConfigField("boolean", "isPre", "true")
- // append latest commit short hash for pre-release
- suffix = "${versionName}.${getGitHash()}-${flavor}" // eg. 19.1.0.b123456-github
+
+ flavorBuiltType = "${getGitHash()}-${flavor}"
+
+ // For x86 and x86_64, the versionNames are already overridden
+ if (versionName.contains(flavorBuiltType)) {
+ suffix = "${versionName}"
+ } else {
+ // append latest commit short hash for pre-release
+ suffix = "${versionName}.${flavorBuiltType}" // eg. 19.1.0.b123456-github
+ }
}
output.versionNameOverride = suffix
- outputFileName = "barinsta_${suffix}.apk"
+ def abi = output.getFilter(com.android.build.OutputFile.ABI)
+ // println(abi + ", " + versionName + ", " + flavor + ", " + builtType + ", " + suffix)
+ outputFileName = abi == null ? "barinsta_${suffix}.apk" : "barinsta_${suffix}_${abi}.apk"
}
}
+
+ packagingOptions {
+ // Exclude file to avoid
+ // Error: Duplicate files during packaging of APK
+ exclude 'META-INF/LICENSE.md'
+ exclude 'META-INF/LICENSE-notice.md'
+ exclude 'META-INF/atomicfu.kotlin_module'
+ }
+
+ testOptions.unitTests {
+ includeAndroidResources = true
+ }
+
}
configurations.all {
@@ -104,49 +163,61 @@ configurations.all {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
- def appcompat_version = "1.2.0"
- def nav_version = '2.3.4'
- def exoplayer_version = '2.13.2'
+ def nav_version = '2.3.5'
+ def exoplayer_version = '2.13.3'
- implementation 'com.google.android.material:material:1.4.0-alpha02'
+ implementation 'com.google.android.material:material:1.4.0-beta01'
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
- implementation "androidx.appcompat:appcompat:$appcompat_version"
- implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
- implementation "androidx.recyclerview:recyclerview:1.2.0-rc01"
+ implementation "androidx.recyclerview:recyclerview:1.2.0"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.preference:preference:1.1.1"
- implementation "androidx.work:work-runtime:2.5.0"
implementation 'androidx.palette:palette:1.0.0'
- implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation 'com.google.guava:guava:27.0.1-android'
+ def core_version = "1.6.0-beta01"
+ implementation "androidx.core:core:$core_version"
+
+ // Fragment
+ implementation "androidx.fragment:fragment-ktx:1.3.4"
+
+ // Lifecycle
+ implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
+
// Room
- def room_version = "2.2.6"
+ def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-guava:$room_version"
+ implementation "androidx.room:room-ktx:$room_version"
+ kapt "androidx.room:room-compiler:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// CameraX
- def camerax_version = "1.1.0-alpha03"
+ def camerax_version = "1.1.0-alpha04"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
- implementation "androidx.camera:camera-view:1.0.0-alpha22"
+ implementation "androidx.camera:camera-view:1.0.0-alpha24"
// EmojiCompat
def emoji_compat_version = "1.1.0"
implementation "androidx.emoji:emoji:$emoji_compat_version"
implementation "androidx.emoji:emoji-appcompat:$emoji_compat_version"
- implementation 'me.austinhuang:AutoLinkTextViewV2:-SNAPSHOT'
+ // Work
+ def work_version = '2.5.0'
+ implementation "androidx.work:work-runtime:$work_version"
+ implementation "androidx.work:work-runtime-ktx:$work_version"
+
+ implementation "ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0"
implementation 'com.facebook.fresco:fresco:2.3.0'
implementation 'com.facebook.fresco:animated-webp:2.3.0'
@@ -157,12 +228,27 @@ dependencies {
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'org.apache.commons:commons-imaging:1.0-alpha2'
- implementation 'com.github.ammargitham:uCrop:2.3-beta'
+
+ implementation 'com.github.skydoves:balloon:1.3.4'
+
+ implementation 'com.github.ammargitham:AutoLinkTextViewV2:3.2.0'
+ implementation 'com.github.ammargitham:uCrop:2.3-native-beta-2'
implementation 'com.github.ammargitham:android-gpuimage:2.1.1-beta4'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
githubImplementation 'io.sentry:sentry-android:4.3.0'
- testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
+ testImplementation "androidx.test.ext:junit-ktx:1.1.2"
+ testImplementation "androidx.test:core-ktx:1.3.0"
+ testImplementation "androidx.arch.core:core-testing:2.1.0"
+ testImplementation "org.robolectric:robolectric:4.5.1"
+
+ androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
+ androidTestImplementation 'androidx.test:core:1.3.0'
+ androidTestImplementation 'com.android.support:support-annotations:28.0.0'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation "androidx.room:room-testing:2.3.0"
+
}
diff --git a/app/schemas/awais.instagrabber.db.AppDatabase/6.json b/app/schemas/awais.instagrabber.db.AppDatabase/6.json
new file mode 100644
index 00000000..4a2f5199
--- /dev/null
+++ b/app/schemas/awais.instagrabber.db.AppDatabase/6.json
@@ -0,0 +1,227 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 6,
+ "identityHash": "232e618b3bfcb4661336b359d036c455",
+ "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": []
+ },
+ {
+ "tableName": "dm_last_notified",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thread_id` TEXT, `last_notified_msg_ts` INTEGER, `last_notified_at` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "threadId",
+ "columnName": "thread_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastNotifiedMsgTs",
+ "columnName": "last_notified_msg_ts",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastNotifiedAt",
+ "columnName": "last_notified_at",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_dm_last_notified_thread_id",
+ "unique": true,
+ "columnNames": [
+ "thread_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_last_notified_thread_id` ON `${TABLE_NAME}` (`thread_id`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "recent_searches",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ig_id` TEXT NOT NULL, `name` TEXT NOT NULL, `username` TEXT, `pic_url` TEXT, `type` TEXT NOT NULL, `last_searched_on` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "igId",
+ "columnName": "ig_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "picUrl",
+ "columnName": "pic_url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastSearchedOn",
+ "columnName": "last_searched_on",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_recent_searches_ig_id_type",
+ "unique": true,
+ "columnNames": [
+ "ig_id",
+ "type"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_recent_searches_ig_id_type` ON `${TABLE_NAME}` (`ig_id`, `type`)"
+ }
+ ],
+ "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, '232e618b3bfcb4661336b359d036c455')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/awais/instagrabber/db/MigrationTest.java b/app/src/androidTest/java/awais/instagrabber/db/MigrationTest.java
new file mode 100644
index 00000000..5156c5ff
--- /dev/null
+++ b/app/src/androidTest/java/awais/instagrabber/db/MigrationTest.java
@@ -0,0 +1,51 @@
+package awais.instagrabber.db;
+
+import androidx.room.Room;
+import androidx.room.migration.Migration;
+import androidx.room.testing.MigrationTestHelper;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+
+import static awais.instagrabber.db.AppDatabase.MIGRATION_4_5;
+import static awais.instagrabber.db.AppDatabase.MIGRATION_5_6;
+
+@RunWith(AndroidJUnit4.class)
+public class MigrationTest {
+ private static final String TEST_DB = "migration-test";
+ private static final Migration[] ALL_MIGRATIONS = new Migration[]{MIGRATION_4_5, MIGRATION_5_6};
+
+ @Rule
+ public MigrationTestHelper helper;
+
+ public MigrationTest() {
+ final String canonicalName = AppDatabase.class.getCanonicalName();
+ assert canonicalName != null;
+ helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
+ canonicalName,
+ new FrameworkSQLiteOpenHelperFactory());
+ }
+
+ @Test
+ public void migrateAll() throws IOException {
+ // Create earliest version of the database. Have to start with 4 since that is the version we migrated to Room.
+ SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 4);
+ db.close();
+
+ // Open latest version of the database. Room will validate the schema
+ // once all migrations execute.
+ AppDatabase appDb = Room.databaseBuilder(InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ AppDatabase.class,
+ TEST_DB)
+ .addMigrations(ALL_MIGRATIONS).build();
+ appDb.getOpenHelper().getWritableDatabase();
+ appDb.close();
+ }
+}
diff --git a/app/src/androidTest/java/awais/instagrabber/db/dao/RecentSearchDaoTest.java b/app/src/androidTest/java/awais/instagrabber/db/dao/RecentSearchDaoTest.java
new file mode 100644
index 00000000..c8a48775
--- /dev/null
+++ b/app/src/androidTest/java/awais/instagrabber/db/dao/RecentSearchDaoTest.java
@@ -0,0 +1,82 @@
+package awais.instagrabber.db.dao;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.room.Room;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.runner.RunWith;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import awais.instagrabber.db.AppDatabase;
+import awais.instagrabber.db.entities.RecentSearch;
+import awais.instagrabber.models.enums.FavoriteType;
+
+@RunWith(AndroidJUnit4.class)
+public class RecentSearchDaoTest {
+ private static final String TAG = RecentSearchDaoTest.class.getSimpleName();
+
+ private RecentSearchDao dao;
+ private AppDatabase db;
+
+ @Before
+ public void createDb() {
+ final Context context = ApplicationProvider.getApplicationContext();
+ db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();
+ dao = db.recentSearchDao();
+ }
+
+ @After
+ public void closeDb() {
+ db.close();
+ }
+
+ @Test
+ public void writeQueryDelete() {
+ final RecentSearch recentSearch = insertRecentSearch("1", "test1", FavoriteType.HASHTAG);
+ final RecentSearch byIgIdAndType = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG);
+ Assertions.assertEquals(recentSearch, byIgIdAndType);
+ dao.deleteRecentSearch(byIgIdAndType);
+ final RecentSearch deleted = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG);
+ Assertions.assertNull(deleted);
+ }
+
+ @Test
+ public void queryAllOrdered() {
+ final List insertListReversed = ImmutableList
+ .builder()
+ .add(insertRecentSearch("1", "test1", FavoriteType.HASHTAG))
+ .add(insertRecentSearch("2", "test2", FavoriteType.LOCATION))
+ .add(insertRecentSearch("3", "test3", FavoriteType.USER))
+ .add(insertRecentSearch("4", "test4", FavoriteType.USER))
+ .add(insertRecentSearch("5", "test5", FavoriteType.USER))
+ .build()
+ .reverse(); // important
+ final List fromDb = dao.getAllRecentSearches();
+ Assertions.assertIterableEquals(insertListReversed, fromDb);
+ }
+
+ @NonNull
+ private RecentSearch insertRecentSearch(final String igId, final String name, final FavoriteType type) {
+ final RecentSearch recentSearch = new RecentSearch(
+ igId,
+ name,
+ null,
+ null,
+ type,
+ LocalDateTime.now()
+ );
+ dao.insertRecentSearch(recentSearch);
+ return recentSearch;
+ }
+}
\ No newline at end of file
diff --git a/app/src/github/res/values-cs/strings.xml b/app/src/github/res/values-cs/strings.xml
index 078fa6a8..f22f68c4 100644
--- a/app/src/github/res/values-cs/strings.xml
+++ b/app/src/github/res/values-cs/strings.xml
@@ -1,6 +1,6 @@
- Enable Sentry
- Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
- Sentry will start on next launch
+ Povolit Sentry
+ Sentry je listener/handler, který zaznamenává chyby a asynchronně je posílá na Sentry.io
+ Sentry se spustí při příštím spuštění
diff --git a/app/src/github/res/values-de/strings.xml b/app/src/github/res/values-de/strings.xml
index 078fa6a8..38331667 100644
--- a/app/src/github/res/values-de/strings.xml
+++ b/app/src/github/res/values-de/strings.xml
@@ -1,6 +1,6 @@
- Enable Sentry
- Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
- Sentry will start on next launch
+ Aktiviere Sentry
+ Sentry ist ein Listener/Handler für Fehler, der den Fehler/das Ereignis asynchron an Sentry.io sendet
+ Sentry startet beim nächsten Start
diff --git a/app/src/github/res/values-ko/strings.xml b/app/src/github/res/values-ko/strings.xml
new file mode 100644
index 00000000..272483b2
--- /dev/null
+++ b/app/src/github/res/values-ko/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Sentry 활성화
+ Sentry는 Sentry.io에 오류를 비동기적으로 보내는 오류 처리기입니다
+ Sentry는 다음 출시에 시작됩니다
+
diff --git a/app/src/github/res/values-nl/strings.xml b/app/src/github/res/values-nl/strings.xml
index 078fa6a8..e043c9b9 100644
--- a/app/src/github/res/values-nl/strings.xml
+++ b/app/src/github/res/values-nl/strings.xml
@@ -1,6 +1,6 @@
- Enable Sentry
- Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
- Sentry will start on next launch
+ Sentry inschakelen
+ Sentry is een luister/handler voor fouten die asynchroon de fout/gebeurtenis versturen naar Sentry.io
+ Sentry zal starten bij de volgende lancering
diff --git a/app/src/github/res/values-pl/strings.xml b/app/src/github/res/values-pl/strings.xml
index 481f46c5..08b8c67e 100644
--- a/app/src/github/res/values-pl/strings.xml
+++ b/app/src/github/res/values-pl/strings.xml
@@ -1,6 +1,6 @@
Włącz Sentry
- Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
+ Sentry jest słuchaczem/obsługą błędów, które asynchronicznie wysyłają błąd/zdarzenie do Sentry.ioSentry rozpocznie się przy następnym uruchomieniu
diff --git a/app/src/github/res/values-ru/strings.xml b/app/src/github/res/values-ru/strings.xml
index 0de74041..ecd5d235 100644
--- a/app/src/github/res/values-ru/strings.xml
+++ b/app/src/github/res/values-ru/strings.xml
@@ -1,6 +1,6 @@
- Включить режим \"часового\"
- \"Часовой\" - это слушатель/обработчик ошибок, который асинхронно отправляет ошибку/событие на Sentry.io
- \"Часовой\" будет запущен при следующем запуске
+ Включить Sentry
+ Sentry - это обработчик событий, который асинхронно отправляет сообщения об ошибках/поломках на Sentry.io
+ Sentry включится при следующем запуске приложения
diff --git a/app/src/github/res/values-sv/strings.xml b/app/src/github/res/values-sv/strings.xml
new file mode 100644
index 00000000..078fa6a8
--- /dev/null
+++ b/app/src/github/res/values-sv/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Enable Sentry
+ Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
+ Sentry will start on next launch
+
diff --git a/app/src/github/res/values-vi/strings.xml b/app/src/github/res/values-vi/strings.xml
index 078fa6a8..8669fe96 100644
--- a/app/src/github/res/values-vi/strings.xml
+++ b/app/src/github/res/values-vi/strings.xml
@@ -1,6 +1,6 @@
- Enable Sentry
- Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
- Sentry will start on next launch
+ Bật Sentry
+ Sentry là một thiết bị nghe/giải quyết cho những lỗi mà gửi những lỗi/sự kiện đến Sentry.io một cách tách biệt
+ Sentry sẽ được bật vào lần khởi động kế tiếp
diff --git a/app/src/github/res/values-zh-rTW/strings.xml b/app/src/github/res/values-zh-rTW/strings.xml
index 078fa6a8..5d778fb0 100644
--- a/app/src/github/res/values-zh-rTW/strings.xml
+++ b/app/src/github/res/values-zh-rTW/strings.xml
@@ -1,6 +1,6 @@
- Enable Sentry
- Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
- Sentry will start on next launch
+ 啟用 Sentry
+ Sentry 將會錯誤報告發送至 Sentry.io
+ 下次啟用應用程式時將會開啟 Sentry
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e6f9c562..4e6d6d3f 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,8 +26,7 @@
+ android:taskAffinity=".Main">
@@ -51,13 +50,14 @@
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java
deleted file mode 100644
index a104a47b..00000000
--- a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package awais.instagrabber;
-
-import android.app.Application;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.os.Handler;
-import android.util.Log;
-
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.imagepipeline.core.ImagePipelineConfig;
-
-import java.net.CookieHandler;
-import java.text.SimpleDateFormat;
-import java.util.UUID;
-
-import awais.instagrabber.utils.Constants;
-import awais.instagrabber.utils.LocaleUtils;
-import awais.instagrabber.utils.SettingsHelper;
-import awais.instagrabber.utils.TextUtils;
-import awaisomereport.CrashReporter;
-
-import static awais.instagrabber.utils.CookieUtils.NET_COOKIE_MANAGER;
-import static awais.instagrabber.utils.Utils.applicationHandler;
-import static awais.instagrabber.utils.Utils.cacheDir;
-import static awais.instagrabber.utils.Utils.clipboardManager;
-import static awais.instagrabber.utils.Utils.datetimeParser;
-import static awais.instagrabber.utils.Utils.settingsHelper;
-
-
-public final class InstaGrabberApplication extends Application {
- private static final String TAG = "InstaGrabberApplication";
-
- @Override
- public void onCreate() {
- super.onCreate();
- CookieHandler.setDefault(NET_COOKIE_MANAGER);
-
- if (settingsHelper == null) {
- settingsHelper = new SettingsHelper(this);
- }
-
- if (!BuildConfig.DEBUG) {
- CrashReporter.get(this).start();
- }
- // logCollector = new LogCollector(this);
-
- if (BuildConfig.DEBUG) {
- try {
- Class.forName("dalvik.system.CloseGuard")
- .getMethod("setEnabled", boolean.class)
- .invoke(null, true);
- } catch (Exception e) {
- Log.e(TAG, "Error", e);
- }
- }
-
- // final Set requestListeners = new HashSet<>();
- // requestListeners.add(new RequestLoggingListener());
- final ImagePipelineConfig imagePipelineConfig = ImagePipelineConfig
- .newBuilder(this)
- // .setMainDiskCacheConfig(diskCacheConfig)
- // .setRequestListeners(requestListeners)
- .setDownsampleEnabled(true)
- .build();
- Fresco.initialize(this, imagePipelineConfig);
- // FLog.setMinimumLoggingLevel(FLog.VERBOSE);
-
- if (applicationHandler == null) {
- applicationHandler = new Handler(getApplicationContext().getMainLooper());
- }
-
- if (cacheDir == null) {
- cacheDir = getCacheDir().getAbsolutePath();
- }
-
- LocaleUtils.setLocale(getBaseContext());
-
- if (clipboardManager == null)
- clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
-
- if (datetimeParser == null)
- datetimeParser = new SimpleDateFormat(
- settingsHelper.getBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED) ?
- settingsHelper.getString(Constants.CUSTOM_DATE_TIME_FORMAT) :
- settingsHelper.getString(Constants.DATE_TIME_FORMAT), LocaleUtils.getCurrentLocale());
-
- if (TextUtils.isEmpty(settingsHelper.getString(Constants.DEVICE_UUID))) {
- settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString());
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.kt b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.kt
new file mode 100644
index 00000000..c45f835f
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.kt
@@ -0,0 +1,71 @@
+package awais.instagrabber
+
+import android.app.Application
+import android.content.ClipboardManager
+import android.util.Log
+import awais.instagrabber.fragments.settings.PreferenceKeys.CUSTOM_DATE_TIME_FORMAT
+import awais.instagrabber.fragments.settings.PreferenceKeys.CUSTOM_DATE_TIME_FORMAT_ENABLED
+import awais.instagrabber.fragments.settings.PreferenceKeys.DATE_TIME_FORMAT
+import awais.instagrabber.utils.*
+import awais.instagrabber.utils.LocaleUtils.currentLocale
+import awais.instagrabber.utils.Utils.settingsHelper
+import awais.instagrabber.utils.extensions.TAG
+import awaisomereport.CrashReporter
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.imagepipeline.core.ImagePipelineConfig
+import java.net.CookieHandler
+import java.time.format.DateTimeFormatter
+import java.util.*
+
+@Suppress("unused")
+class InstaGrabberApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ CookieHandler.setDefault(NET_COOKIE_MANAGER)
+ settingsHelper = SettingsHelper(this)
+ setupCrashReporter()
+ setupCloseGuard()
+ setupFresco()
+ Utils.cacheDir = cacheDir.absolutePath
+ Utils.clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+ LocaleUtils.setLocale(baseContext)
+ val pattern = if (settingsHelper.getBoolean(CUSTOM_DATE_TIME_FORMAT_ENABLED)) {
+ settingsHelper.getString(CUSTOM_DATE_TIME_FORMAT)
+ } else {
+ settingsHelper.getString(DATE_TIME_FORMAT)
+ }
+ TextUtils.setFormatter(DateTimeFormatter.ofPattern(pattern, currentLocale))
+ if (TextUtils.isEmpty(settingsHelper.getString(Constants.DEVICE_UUID))) {
+ settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString())
+ }
+ }
+
+ private fun setupCrashReporter() {
+ if (BuildConfig.DEBUG) return
+ CrashReporter.get(this).start()
+ // logCollector = new LogCollector(this);
+ }
+
+ private fun setupCloseGuard() {
+ if (!BuildConfig.DEBUG) return
+ try {
+ Class.forName("dalvik.system.CloseGuard")
+ .getMethod("setEnabled", Boolean::class.javaPrimitiveType)
+ .invoke(null, true)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error", e)
+ }
+ }
+
+ private fun setupFresco() {
+ // final Set requestListeners = new HashSet<>();
+ // requestListeners.add(new RequestLoggingListener());
+ val imagePipelineConfig = ImagePipelineConfig
+ .newBuilder(this) // .setMainDiskCacheConfig(diskCacheConfig)
+ // .setRequestListeners(requestListeners)
+ .setDownsampleEnabled(true)
+ .build()
+ Fresco.initialize(this, imagePipelineConfig)
+ // FLog.setMinimumLoggingLevel(FLog.VERBOSE);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.java b/app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.java
deleted file mode 100755
index aeaad606..00000000
--- a/app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package awais.instagrabber.activities;
-
-import android.os.Bundle;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-
-import awais.instagrabber.utils.LocaleUtils;
-import awais.instagrabber.utils.ThemeUtils;
-
-public abstract class BaseLanguageActivity extends AppCompatActivity {
- protected BaseLanguageActivity() {
- LocaleUtils.updateConfig(this);
- }
-
- @Override
- protected void onCreate(@Nullable final Bundle savedInstanceState) {
- ThemeUtils.changeTheme(this);
- super.onCreate(savedInstanceState);
- }
-}
diff --git a/app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.kt b/app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.kt
new file mode 100755
index 00000000..f61171ac
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.kt
@@ -0,0 +1,18 @@
+package awais.instagrabber.activities
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import awais.instagrabber.utils.LocaleUtils
+import awais.instagrabber.utils.ThemeUtils
+
+abstract class BaseLanguageActivity protected constructor() : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ ThemeUtils.changeTheme(this)
+ super.onCreate(savedInstanceState)
+ }
+
+ init {
+ @Suppress("LeakingThis")
+ LocaleUtils.updateConfig(this)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/activities/CameraActivity.java b/app/src/main/java/awais/instagrabber/activities/CameraActivity.java
deleted file mode 100644
index 2d987b19..00000000
--- a/app/src/main/java/awais/instagrabber/activities/CameraActivity.java
+++ /dev/null
@@ -1,281 +0,0 @@
-package awais.instagrabber.activities;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.hardware.display.DisplayManager;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.LayoutInflater;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.camera.core.CameraInfoUnavailableException;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.ImageCaptureException;
-import androidx.camera.core.Preview;
-import androidx.camera.lifecycle.ProcessCameraProvider;
-import androidx.core.content.ContextCompat;
-import androidx.documentfile.provider.DocumentFile;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.text.SimpleDateFormat;
-import java.util.Locale;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-import awais.instagrabber.databinding.ActivityCameraBinding;
-import awais.instagrabber.utils.DownloadUtils;
-import awais.instagrabber.utils.PermissionUtils;
-import awais.instagrabber.utils.Utils;
-
-public class CameraActivity extends BaseLanguageActivity {
- private static final String TAG = CameraActivity.class.getSimpleName();
- private static final int CAMERA_REQUEST_CODE = 100;
- private static final String FILE_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS";
- private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat(FILE_FORMAT, Locale.US);
-
- private ActivityCameraBinding binding;
- private ImageCapture imageCapture;
- private DocumentFile outputDirectory;
- private ExecutorService cameraExecutor;
- private int displayId = -1;
-
- private final DisplayManager.DisplayListener displayListener = new DisplayManager.DisplayListener() {
- @Override
- public void onDisplayAdded(final int displayId) {}
-
- @Override
- public void onDisplayRemoved(final int displayId) {}
-
- @Override
- public void onDisplayChanged(final int displayId) {
- if (displayId == CameraActivity.this.displayId) {
- imageCapture.setTargetRotation(binding.getRoot().getDisplay().getRotation());
- }
- }
- };
- private DisplayManager displayManager;
- private ProcessCameraProvider cameraProvider;
- private int lensFacing;
-
- @Override
- protected void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding = ActivityCameraBinding.inflate(LayoutInflater.from(getBaseContext()));
- setContentView(binding.getRoot());
- Utils.transparentStatusBar(this, true, false);
- displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
- outputDirectory = DownloadUtils.getCameraDir();
- cameraExecutor = Executors.newSingleThreadExecutor();
- displayManager.registerDisplayListener(displayListener, null);
- binding.viewFinder.post(() -> {
- displayId = binding.viewFinder.getDisplay().getDisplayId();
- updateUi();
- checkPermissionsAndSetupCamera();
- });
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- // Make sure that all permissions are still present, since the
- // user could have removed them while the app was in paused state.
- if (!PermissionUtils.hasCameraPerms(this)) {
- PermissionUtils.requestCameraPerms(this, CAMERA_REQUEST_CODE);
- }
- }
-
- @Override
- public void onConfigurationChanged(@NonNull final Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- // Redraw the camera UI controls
- updateUi();
-
- // Enable or disable switching between cameras
- updateCameraSwitchButton();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- Utils.transparentStatusBar(this, false, false);
- cameraExecutor.shutdown();
- displayManager.unregisterDisplayListener(displayListener);
- }
-
- private void updateUi() {
- binding.cameraCaptureButton.setOnClickListener(v -> {
- try {
- takePhoto();
- } catch (IOException e) {
- Log.e(TAG, "updateUi: ", e);
- }
- });
- // Disable the button until the camera is set up
- binding.switchCamera.setEnabled(false);
- // Listener for button used to switch cameras. Only called if the button is enabled
- binding.switchCamera.setOnClickListener(v -> {
- lensFacing = CameraSelector.LENS_FACING_FRONT == lensFacing ? CameraSelector.LENS_FACING_BACK
- : CameraSelector.LENS_FACING_FRONT;
- // Re-bind use cases to update selected camera
- bindCameraUseCases();
- });
- binding.close.setOnClickListener(v -> {
- setResult(Activity.RESULT_CANCELED);
- finish();
- });
- }
-
- private void checkPermissionsAndSetupCamera() {
- if (PermissionUtils.hasCameraPerms(this)) {
- setupCamera();
- return;
- }
- PermissionUtils.requestCameraPerms(this, CAMERA_REQUEST_CODE);
- }
-
- @Override
- public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
- if (requestCode == CAMERA_REQUEST_CODE) {
- if (PermissionUtils.hasCameraPerms(this)) {
- setupCamera();
- }
- }
- }
-
- private void setupCamera() {
- final ListenableFuture cameraProviderFuture = ProcessCameraProvider.getInstance(this);
- cameraProviderFuture.addListener(() -> {
- try {
- cameraProvider = cameraProviderFuture.get();
- // Select lensFacing depending on the available cameras
- lensFacing = -1;
- if (hasBackCamera()) {
- lensFacing = CameraSelector.LENS_FACING_BACK;
- } else if (hasFrontCamera()) {
- lensFacing = CameraSelector.LENS_FACING_FRONT;
- }
- if (lensFacing == -1) {
- throw new IllegalStateException("Back and front camera are unavailable");
- }
- // Enable or disable switching between cameras
- updateCameraSwitchButton();
- // Build and bind the camera use cases
- bindCameraUseCases();
- } catch (ExecutionException | InterruptedException | CameraInfoUnavailableException e) {
- Log.e(TAG, "setupCamera: ", e);
- }
-
- }, ContextCompat.getMainExecutor(this));
- }
-
- private void bindCameraUseCases() {
- final int rotation = binding.viewFinder.getDisplay().getRotation();
-
- // CameraSelector
- final CameraSelector cameraSelector = new CameraSelector.Builder()
- .requireLensFacing(lensFacing)
- .build();
-
- // Preview
- final Preview preview = new Preview.Builder()
- // Set initial target rotation
- .setTargetRotation(rotation)
- .build();
-
- // ImageCapture
- imageCapture = new ImageCapture.Builder()
- .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
- // Set initial target rotation, we will have to call this again if rotation changes
- // during the lifecycle of this use case
- .setTargetRotation(rotation)
- .build();
-
- cameraProvider.unbindAll();
- cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture);
-
- preview.setSurfaceProvider(binding.viewFinder.getSurfaceProvider());
- }
-
- private void takePhoto() throws IOException {
- if (imageCapture == null) return;
- final String extension = "jpg";
- final String fileName = SIMPLE_DATE_FORMAT.format(System.currentTimeMillis()) + "." + extension;
- // final File photoFile = new File(outputDirectory, fileName);
- final String mimeType = "image/jpg";
- final DocumentFile photoFile = outputDirectory.createFile(mimeType, fileName);
- if (photoFile == null) {
- Log.e(TAG, "takePhoto: photoFile is null!");
- return;
- }
- final OutputStream outputStream = getContentResolver().openOutputStream(photoFile.getUri());
- if (outputStream == null) return;
- final ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(outputStream).build();
- imageCapture.takePicture(
- outputFileOptions,
- cameraExecutor,
- new ImageCapture.OnImageSavedCallback() {
- @Override
- public void onImageSaved(@NonNull final ImageCapture.OutputFileResults outputFileResults) {
- try { outputStream.close(); } catch (IOException ignored) {}
- final Intent intent = new Intent();
- intent.setData(photoFile.getUri());
- setResult(Activity.RESULT_OK, intent);
- finish();
- Log.d(TAG, "onImageSaved: " + photoFile.getUri());
- }
-
- @Override
- public void onError(@NonNull final ImageCaptureException exception) {
- Log.e(TAG, "onError: ", exception);
- try { outputStream.close(); } catch (IOException ignored) {}
- }
- }
- );
- // We can only change the foreground Drawable using API level 23+ API
- // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- // // Display flash animation to indicate that photo was captured
- // final ConstraintLayout container = binding.getRoot();
- // container.postDelayed(() -> {
- // container.setForeground(new ColorDrawable(Color.WHITE));
- // container.postDelayed(() -> container.setForeground(null), 50);
- // }, 100);
- // }
- }
-
- /**
- * Enabled or disabled a button to switch cameras depending on the available cameras
- */
- private void updateCameraSwitchButton() {
- try {
- binding.switchCamera.setEnabled(hasBackCamera() && hasFrontCamera());
- } catch (CameraInfoUnavailableException e) {
- binding.switchCamera.setEnabled(false);
- }
- }
-
- /**
- * Returns true if the device has an available back camera. False otherwise
- */
- private boolean hasBackCamera() throws CameraInfoUnavailableException {
- if (cameraProvider == null) return false;
- return cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA);
- }
-
- /**
- * Returns true if the device has an available front camera. False otherwise
- */
- private boolean hasFrontCamera() throws CameraInfoUnavailableException {
- if (cameraProvider == null) {
- return false;
- }
- return cameraProvider.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA);
- }
-}
diff --git a/app/src/main/java/awais/instagrabber/activities/CameraActivity.kt b/app/src/main/java/awais/instagrabber/activities/CameraActivity.kt
new file mode 100644
index 00000000..cfdf4516
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/activities/CameraActivity.kt
@@ -0,0 +1,248 @@
+package awais.instagrabber.activities
+
+import android.content.Intent
+import android.content.res.Configuration
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.DisplayListener
+import android.media.MediaScannerConnection
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.webkit.MimeTypeMap
+import androidx.camera.core.*
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.core.content.ContextCompat
+import awais.instagrabber.databinding.ActivityCameraBinding
+import awais.instagrabber.utils.DirectoryUtils
+import awais.instagrabber.utils.PermissionUtils
+import awais.instagrabber.utils.Utils
+import awais.instagrabber.utils.extensions.TAG
+import com.google.common.io.Files
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.*
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class CameraActivity : BaseLanguageActivity() {
+ private lateinit var binding: ActivityCameraBinding
+ private lateinit var outputDirectory: File
+ private lateinit var displayManager: DisplayManager
+ private lateinit var cameraExecutor: ExecutorService
+
+ private var imageCapture: ImageCapture? = null
+ private var displayId = -1
+ private var cameraProvider: ProcessCameraProvider? = null
+ private var lensFacing = 0
+
+ private val cameraRequestCode = 100
+ private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
+ private val displayListener: DisplayListener = object : DisplayListener {
+ override fun onDisplayAdded(displayId: Int) {}
+ override fun onDisplayRemoved(displayId: Int) {}
+ override fun onDisplayChanged(displayId: Int) {
+ if (displayId == this@CameraActivity.displayId) {
+ imageCapture?.targetRotation = binding.root.display.rotation
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityCameraBinding.inflate(LayoutInflater.from(baseContext))
+ setContentView(binding.root)
+ Utils.transparentStatusBar(this, true, false)
+ displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
+ outputDirectory = DirectoryUtils.getOutputMediaDirectory(this, "Camera")
+ cameraExecutor = Executors.newSingleThreadExecutor()
+ displayManager.registerDisplayListener(displayListener, null)
+ binding.viewFinder.post {
+ displayId = binding.viewFinder.display.displayId
+ updateUi()
+ checkPermissionsAndSetupCamera()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ // Make sure that all permissions are still present, since the
+ // user could have removed them while the app was in paused state.
+ if (!PermissionUtils.hasCameraPerms(this)) {
+ PermissionUtils.requestCameraPerms(this, cameraRequestCode)
+ }
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ // Redraw the camera UI controls
+ updateUi()
+
+ // Enable or disable switching between cameras
+ updateCameraSwitchButton()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ Utils.transparentStatusBar(this, false, false)
+ cameraExecutor.shutdown()
+ displayManager.unregisterDisplayListener(displayListener)
+ }
+
+ private fun updateUi() {
+ binding.cameraCaptureButton.setOnClickListener { takePhoto() }
+ // Disable the button until the camera is set up
+ binding.switchCamera.isEnabled = false
+ // Listener for button used to switch cameras. Only called if the button is enabled
+ binding.switchCamera.setOnClickListener {
+ lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) CameraSelector.LENS_FACING_BACK else CameraSelector.LENS_FACING_FRONT
+ // Re-bind use cases to update selected camera
+ bindCameraUseCases()
+ }
+ binding.close.setOnClickListener {
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ }
+
+ private fun checkPermissionsAndSetupCamera() {
+ if (PermissionUtils.hasCameraPerms(this)) {
+ setupCamera()
+ return
+ }
+ PermissionUtils.requestCameraPerms(this, cameraRequestCode)
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == cameraRequestCode) {
+ if (PermissionUtils.hasCameraPerms(this)) {
+ setupCamera()
+ }
+ }
+ }
+
+ private fun setupCamera() {
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
+ cameraProviderFuture.addListener({
+ try {
+ cameraProvider = cameraProviderFuture.get()
+ // Select lensFacing depending on the available cameras
+ lensFacing = -1
+ if (hasBackCamera()) {
+ lensFacing = CameraSelector.LENS_FACING_BACK
+ } else if (hasFrontCamera()) {
+ lensFacing = CameraSelector.LENS_FACING_FRONT
+ }
+ check(lensFacing != -1) { "Back and front camera are unavailable" }
+ // Enable or disable switching between cameras
+ updateCameraSwitchButton()
+ // Build and bind the camera use cases
+ bindCameraUseCases()
+ } catch (e: ExecutionException) {
+ Log.e(TAG, "setupCamera: ", e)
+ } catch (e: InterruptedException) {
+ Log.e(TAG, "setupCamera: ", e)
+ } catch (e: CameraInfoUnavailableException) {
+ Log.e(TAG, "setupCamera: ", e)
+ }
+ }, ContextCompat.getMainExecutor(this))
+ }
+
+ private fun bindCameraUseCases() {
+ val rotation = binding.viewFinder.display.rotation
+
+ // CameraSelector
+ val cameraSelector = CameraSelector.Builder()
+ .requireLensFacing(lensFacing)
+ .build()
+
+ // Preview
+ val preview = Preview.Builder() // Set initial target rotation
+ .setTargetRotation(rotation)
+ .build()
+
+ // ImageCapture
+ imageCapture = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // Set initial target rotation, we will have to call this again if rotation changes
+ // during the lifecycle of this use case
+ .setTargetRotation(rotation)
+ .build()
+ cameraProvider?.unbindAll()
+ cameraProvider?.bindToLifecycle(this, cameraSelector, preview, imageCapture)
+ preview.setSurfaceProvider(binding.viewFinder.surfaceProvider)
+ }
+
+ private fun takePhoto() {
+ if (imageCapture == null) return
+ val photoFile = File(outputDirectory, simpleDateFormat.format(System.currentTimeMillis()) + ".jpg")
+ val outputFileOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
+ imageCapture?.takePicture(
+ outputFileOptions,
+ cameraExecutor,
+ object : ImageCapture.OnImageSavedCallback {
+ @Suppress("UnstableApiUsage")
+ override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
+ val uri = Uri.fromFile(photoFile)
+ val mimeType = MimeTypeMap.getSingleton()
+ .getMimeTypeFromExtension(Files.getFileExtension(photoFile.name))
+ MediaScannerConnection.scanFile(
+ this@CameraActivity,
+ arrayOf(photoFile.absolutePath),
+ arrayOf(mimeType)
+ ) { _: String?, uri1: Uri? ->
+ Log.d(TAG, "onImageSaved: scan complete")
+ val intent = Intent()
+ intent.data = uri1
+ setResult(RESULT_OK, intent)
+ finish()
+ }
+ Log.d(TAG, "onImageSaved: $uri")
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ Log.e(TAG, "onError: ", exception)
+ }
+ }
+ )
+ // We can only change the foreground Drawable using API level 23+ API
+ // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // // Display flash animation to indicate that photo was captured
+ // final ConstraintLayout container = binding.getRoot();
+ // container.postDelayed(() -> {
+ // container.setForeground(new ColorDrawable(Color.WHITE));
+ // container.postDelayed(() -> container.setForeground(null), 50);
+ // }, 100);
+ // }
+ }
+
+ /**
+ * Enabled or disabled a button to switch cameras depending on the available cameras
+ */
+ private fun updateCameraSwitchButton() {
+ try {
+ binding.switchCamera.isEnabled = hasBackCamera() && hasFrontCamera()
+ } catch (e: CameraInfoUnavailableException) {
+ binding.switchCamera.isEnabled = false
+ }
+ }
+
+ /**
+ * Returns true if the device has an available back camera. False otherwise
+ */
+ @Throws(CameraInfoUnavailableException::class)
+ private fun hasBackCamera(): Boolean {
+ return if (cameraProvider == null) false else cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
+ }
+
+ /**
+ * Returns true if the device has an available front camera. False otherwise
+ */
+ @Throws(CameraInfoUnavailableException::class)
+ private fun hasFrontCamera(): Boolean {
+ return if (cameraProvider == null) {
+ false
+ } else cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/activities/DirectDownload.java b/app/src/main/java/awais/instagrabber/activities/DirectDownload.java
deleted file mode 100644
index 94e02280..00000000
--- a/app/src/main/java/awais/instagrabber/activities/DirectDownload.java
+++ /dev/null
@@ -1,149 +0,0 @@
-package awais.instagrabber.activities;
-
-import android.app.Notification;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.view.WindowManager;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-
-import awais.instagrabber.R;
-import awais.instagrabber.asyncs.PostFetcher;
-import awais.instagrabber.interfaces.FetchListener;
-import awais.instagrabber.models.IntentModel;
-import awais.instagrabber.models.enums.IntentModelType;
-import awais.instagrabber.repositories.responses.Media;
-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;
- private static final int STORAGE_PERM_REQUEST_CODE = 8020;
-
- private boolean contextFound = false;
- private Intent intent;
- private Context context;
- private NotificationManagerCompat notificationManager;
-
- @Override
- protected void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_direct);
- }
-
- @Override
- public void onWindowAttributesChanged(final WindowManager.LayoutParams params) {
- super.onWindowAttributesChanged(params);
- if (!contextFound) {
- intent = getIntent();
- context = getApplicationContext();
- if (intent != null && context != null) {
- contextFound = true;
- checkPermissions();
- }
- }
- }
-
- @Override
- public Resources getResources() {
- if (!contextFound) {
- intent = getIntent();
- context = getApplicationContext();
- if (intent != null && context != null) {
- contextFound = true;
- checkPermissions();
- }
- }
- return super.getResources();
- }
-
- private synchronized void checkPermissions() {
- // if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
- doDownload();
- // return;
- // }
- // ActivityCompat.requestPermissions(this, DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
- }
-
- @Override
- public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
- final boolean granted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
- if (requestCode == STORAGE_PERM_REQUEST_CODE && granted) {
- doDownload();
- }
- }
-
- private synchronized void doDownload() {
- CookieUtils.setupCookies(Utils.settingsHelper.getString(Constants.COOKIE));
- notificationManager = NotificationManagerCompat.from(getApplicationContext());
- final Intent intent = getIntent();
- final String action = intent.getAction();
- if (TextUtils.isEmpty(action) || Intent.ACTION_MAIN.equals(action)) {
- finish();
- return;
- }
- boolean error = true;
-
- String data = null;
- final Bundle extras = intent.getExtras();
- if (extras != null) {
- final Object extraData = extras.get(Intent.EXTRA_TEXT);
- if (extraData != null) {
- error = false;
- data = extraData.toString();
- }
- }
- if (error) {
- final Uri intentData = intent.getData();
- if (intentData != null) data = intentData.toString();
- }
- if (data == null || TextUtils.isEmpty(data)) {
- finish();
- return;
- }
- final IntentModel model = IntentUtils.parseUrl(data);
- if (model == null || model.getType() != IntentModelType.POST) {
- finish();
- return;
- }
- final String text = model.getText();
- new PostFetcher(text, new FetchListener() {
- @Override
- public void doBefore() {
- if (notificationManager == null) return;
- final Notification fetchingPostNotification = new NotificationCompat.Builder(getApplicationContext(), Constants.DOWNLOAD_CHANNEL_ID)
- .setCategory(NotificationCompat.CATEGORY_STATUS)
- .setSmallIcon(R.drawable.ic_download)
- .setAutoCancel(false)
- .setPriority(NotificationCompat.PRIORITY_MIN)
- .setContentText(getString(R.string.direct_download_loading))
- .build();
- notificationManager.notify(NOTIFICATION_ID, fetchingPostNotification);
- }
-
- @Override
- public void onResult(final Media result) {
- if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
- if (result == null) {
- finish();
- return;
- }
- DownloadUtils.download(getApplicationContext(), result);
- finish();
- }
- }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/activities/Login.java b/app/src/main/java/awais/instagrabber/activities/Login.java
deleted file mode 100755
index f0681486..00000000
--- a/app/src/main/java/awais/instagrabber/activities/Login.java
+++ /dev/null
@@ -1,140 +0,0 @@
-package awais.instagrabber.activities;
-
-import android.annotation.SuppressLint;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.os.Build;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.webkit.CookieManager;
-import android.webkit.CookieSyncManager;
-import android.webkit.WebChromeClient;
-import android.webkit.WebSettings;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import android.widget.Toast;
-
-import androidx.annotation.Nullable;
-
-import awais.instagrabber.R;
-import awais.instagrabber.databinding.ActivityLoginBinding;
-import awais.instagrabber.utils.Constants;
-import awais.instagrabber.utils.CookieUtils;
-import awais.instagrabber.utils.TextUtils;
-
-public final class Login extends BaseLanguageActivity implements View.OnClickListener {
- private final WebViewClient webViewClient = new WebViewClient() {
- @Override
- public void onPageStarted(final WebView view, final String url, final Bitmap favicon) {
- webViewUrl = url;
- }
-
- @Override
- public void onPageFinished(final WebView view, final String url) {
- webViewUrl = url;
- final String mainCookie = CookieUtils.getCookie(url);
- if (TextUtils.isEmpty(mainCookie) || !mainCookie.contains("; ds_user_id=")) {
- ready = true;
- return;
- }
- if (mainCookie.contains("; ds_user_id=") && ready) {
- returnCookieResult(mainCookie);
- }
- }
- };
-
- private void returnCookieResult(final String mainCookie) {
- final Intent intent = new Intent();
- intent.putExtra("cookie", mainCookie);
- setResult(Constants.LOGIN_RESULT_CODE, intent);
- finish();
- }
-
- private final WebChromeClient webChromeClient = new WebChromeClient();
- private String webViewUrl;
- private boolean ready = false;
- private ActivityLoginBinding loginBinding;
-
- @Override
- protected void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- loginBinding = ActivityLoginBinding.inflate(LayoutInflater.from(getApplicationContext()));
- setContentView(loginBinding.getRoot());
-
- initWebView();
-
- loginBinding.cookies.setOnClickListener(this);
- loginBinding.refresh.setOnClickListener(this);
- }
-
- @Override
- public void onClick(final View v) {
- if (v == loginBinding.refresh) {
- loginBinding.webView.loadUrl("https://instagram.com/");
- return;
- }
- if (v == loginBinding.cookies) {
- final String mainCookie = CookieUtils.getCookie(webViewUrl);
- if (TextUtils.isEmpty(mainCookie) || !mainCookie.contains("; ds_user_id=")) {
- Toast.makeText(this, R.string.login_error_loading_cookies, Toast.LENGTH_SHORT).show();
- return;
- }
- returnCookieResult(mainCookie);
- }
- }
-
- @SuppressLint("SetJavaScriptEnabled")
- private void initWebView() {
- if (loginBinding != null) {
- loginBinding.webView.setWebChromeClient(webChromeClient);
- loginBinding.webView.setWebViewClient(webViewClient);
- final WebSettings webSettings = loginBinding.webView.getSettings();
- if (webSettings != null) {
- webSettings.setUserAgentString("Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Mobile Safari/537.36");
- webSettings.setJavaScriptEnabled(true);
- webSettings.setDomStorageEnabled(true);
- webSettings.setSupportZoom(true);
- webSettings.setBuiltInZoomControls(true);
- webSettings.setDisplayZoomControls(false);
- webSettings.setLoadWithOverviewMode(true);
- webSettings.setUseWideViewPort(true);
- webSettings.setAllowFileAccessFromFileURLs(true);
- webSettings.setAllowUniversalAccessFromFileURLs(true);
- webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
- CookieManager.getInstance().removeAllCookies(null);
- CookieManager.getInstance().flush();
- } else {
- CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(getApplicationContext());
- cookieSyncMngr.startSync();
- CookieManager cookieManager = CookieManager.getInstance();
- cookieManager.removeAllCookie();
- cookieManager.removeSessionCookie();
- cookieSyncMngr.stopSync();
- cookieSyncMngr.sync();
- }
- loginBinding.webView.loadUrl("https://instagram.com/");
- }
- }
-
- @Override
- protected void onPause() {
- if (loginBinding != null) loginBinding.webView.onPause();
- super.onPause();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- if (loginBinding != null) loginBinding.webView.onResume();
- }
-
- @Override
- protected void onDestroy() {
- if (loginBinding != null) loginBinding.webView.destroy();
- super.onDestroy();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/activities/Login.kt b/app/src/main/java/awais/instagrabber/activities/Login.kt
new file mode 100755
index 00000000..d240b7b1
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/activities/Login.kt
@@ -0,0 +1,119 @@
+package awais.instagrabber.activities
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.graphics.Bitmap
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.webkit.*
+import android.widget.Toast
+import awais.instagrabber.R
+import awais.instagrabber.databinding.ActivityLoginBinding
+import awais.instagrabber.utils.Constants
+import awais.instagrabber.utils.getCookie
+
+class Login : BaseLanguageActivity(), View.OnClickListener {
+ private var webViewUrl: String? = null
+ private var ready = false
+ private lateinit var loginBinding: ActivityLoginBinding
+
+ private val webChromeClient = WebChromeClient()
+ private val webViewClient: WebViewClient = object : WebViewClient() {
+ override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
+ webViewUrl = url
+ }
+
+ override fun onPageFinished(view: WebView, url: String) {
+ webViewUrl = url
+ val mainCookie = getCookie(url)
+ if (mainCookie.isNullOrBlank() || !mainCookie.contains("; ds_user_id=")) {
+ ready = true
+ return
+ }
+ if (mainCookie.contains("; ds_user_id=") && ready) {
+ returnCookieResult(mainCookie)
+ }
+ }
+ }
+
+ private fun returnCookieResult(mainCookie: String?) {
+ val intent = Intent()
+ intent.putExtra("cookie", mainCookie)
+ setResult(Constants.LOGIN_RESULT_CODE, intent)
+ finish()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ loginBinding = ActivityLoginBinding.inflate(LayoutInflater.from(applicationContext))
+ setContentView(loginBinding.root)
+ initWebView()
+ loginBinding.cookies.setOnClickListener(this)
+ loginBinding.refresh.setOnClickListener(this)
+ }
+
+ override fun onClick(v: View) {
+ if (v === loginBinding.refresh) {
+ loginBinding.webView.loadUrl("https://instagram.com/")
+ return
+ }
+ if (v === loginBinding.cookies) {
+ val mainCookie = getCookie(webViewUrl)
+ if (mainCookie.isNullOrBlank() || !mainCookie.contains("; ds_user_id=")) {
+ Toast.makeText(this, R.string.login_error_loading_cookies, Toast.LENGTH_SHORT).show()
+ return
+ }
+ returnCookieResult(mainCookie)
+ }
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private fun initWebView() {
+ loginBinding.webView.webChromeClient = webChromeClient
+ loginBinding.webView.webViewClient = webViewClient
+ val webSettings = loginBinding.webView.settings
+ webSettings.userAgentString =
+ "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Mobile Safari/537.36"
+ webSettings.javaScriptEnabled = true
+ webSettings.domStorageEnabled = true
+ webSettings.setSupportZoom(true)
+ webSettings.builtInZoomControls = true
+ webSettings.displayZoomControls = false
+ webSettings.loadWithOverviewMode = true
+ webSettings.useWideViewPort = true
+ webSettings.allowFileAccessFromFileURLs = true
+ webSettings.allowUniversalAccessFromFileURLs = true
+ webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
+ CookieManager.getInstance().removeAllCookies(null)
+ CookieManager.getInstance().flush()
+ } else {
+ val cookieSyncMngr = CookieSyncManager.createInstance(applicationContext)
+ cookieSyncMngr.startSync()
+ val cookieManager = CookieManager.getInstance()
+ cookieManager.removeAllCookie()
+ cookieManager.removeSessionCookie()
+ cookieSyncMngr.stopSync()
+ cookieSyncMngr.sync()
+ }
+ loginBinding.webView.loadUrl("https://instagram.com/")
+ }
+
+ override fun onPause() {
+ loginBinding.webView.onPause()
+ super.onPause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ loginBinding.webView.onResume()
+ }
+
+ override fun onDestroy() {
+ loginBinding.webView.destroy()
+ super.onDestroy()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java
deleted file mode 100644
index 7f4904d4..00000000
--- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java
+++ /dev/null
@@ -1,973 +0,0 @@
-package awais.instagrabber.activities;
-
-import android.animation.LayoutTransition;
-import android.annotation.SuppressLint;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.database.MatrixCursor;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.provider.BaseColumns;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.WindowManager;
-import android.widget.AutoCompleteTextView;
-import android.widget.Toast;
-
-import androidx.annotation.IdRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.widget.SearchView;
-import androidx.appcompat.widget.Toolbar;
-import androidx.coordinatorlayout.widget.CoordinatorLayout;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.provider.FontRequest;
-import androidx.emoji.text.EmojiCompat;
-import androidx.emoji.text.FontRequestEmojiCompatConfig;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Observer;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.navigation.NavBackStackEntry;
-import androidx.navigation.NavController;
-import androidx.navigation.NavDestination;
-import androidx.navigation.ui.NavigationUI;
-
-import com.google.android.material.appbar.AppBarLayout;
-import com.google.android.material.appbar.CollapsingToolbarLayout;
-import com.google.android.material.badge.BadgeDrawable;
-import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
-import com.google.android.material.bottomnavigation.BottomNavigationView;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterators;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.List;
-import java.util.UUID;
-import java.util.stream.Collectors;
-
-import awais.instagrabber.BuildConfig;
-import awais.instagrabber.R;
-import awais.instagrabber.adapters.SuggestionsAdapter;
-import awais.instagrabber.asyncs.PostFetcher;
-import awais.instagrabber.customviews.emoji.EmojiVariantManager;
-import awais.instagrabber.databinding.ActivityMainBinding;
-import awais.instagrabber.fragments.PostViewV2Fragment;
-import awais.instagrabber.fragments.directmessages.DirectMessageInboxFragmentDirections;
-import awais.instagrabber.fragments.main.FeedFragment;
-import awais.instagrabber.fragments.settings.PreferenceKeys;
-import awais.instagrabber.models.IntentModel;
-import awais.instagrabber.models.Tab;
-import awais.instagrabber.models.enums.SuggestionType;
-import awais.instagrabber.repositories.responses.search.SearchItem;
-import awais.instagrabber.repositories.responses.search.SearchResponse;
-import awais.instagrabber.services.ActivityCheckerService;
-import awais.instagrabber.services.DMSyncAlarmReceiver;
-import awais.instagrabber.utils.AppExecutors;
-import awais.instagrabber.utils.Constants;
-import awais.instagrabber.utils.CookieUtils;
-import awais.instagrabber.utils.DownloadUtils;
-import awais.instagrabber.utils.FlavorTown;
-import awais.instagrabber.utils.IntentUtils;
-import awais.instagrabber.utils.TextUtils;
-import awais.instagrabber.utils.Utils;
-import awais.instagrabber.utils.emoji.EmojiParser;
-import awais.instagrabber.viewmodels.AppStateViewModel;
-import awais.instagrabber.viewmodels.DirectInboxViewModel;
-import awais.instagrabber.webservices.RetrofitFactory;
-import awais.instagrabber.webservices.SearchService;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-import static awais.instagrabber.utils.Constants.EXTRA_INITIAL_URI;
-import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController;
-import static awais.instagrabber.utils.Utils.settingsHelper;
-
-public class MainActivity extends BaseLanguageActivity implements FragmentManager.OnBackStackChangedListener {
- private static final String TAG = "MainActivity";
- private static final String FIRST_FRAGMENT_GRAPH_INDEX_KEY = "firstFragmentGraphIndex";
- private static final String LAST_SELECT_NAV_MENU_ID = "lastSelectedNavMenuId";
-
- private ActivityMainBinding binding;
- private LiveData currentNavControllerLiveData;
- private MenuItem searchMenuItem;
- private SuggestionsAdapter suggestionAdapter;
- private AutoCompleteTextView searchAutoComplete;
- private SearchView searchView;
- private SearchService searchService;
- private boolean showSearch = true;
- private Handler suggestionsFetchHandler;
- private int firstFragmentGraphIndex;
- private int lastSelectedNavMenuId;
- private boolean isActivityCheckerServiceBound = false;
- private boolean isBackStackEmpty = false;
- private boolean isLoggedIn;
- private HideBottomViewOnScrollBehavior behavior;
- private List currentTabs;
- private List showBottomViewDestinations = Collections.emptyList();
-
- private final ServiceConnection serviceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(final ComponentName name, final IBinder service) {
- // final ActivityCheckerService.LocalBinder binder = (ActivityCheckerService.LocalBinder) service;
- // final ActivityCheckerService activityCheckerService = binder.getService();
- isActivityCheckerServiceBound = true;
- }
-
- @Override
- public void onServiceDisconnected(final ComponentName name) {
- isActivityCheckerServiceBound = false;
- }
- };
-
- @Override
- protected void onCreate(@Nullable final Bundle savedInstanceState) {
- try {
- DownloadUtils.init(this);
- } catch (DownloadUtils.ReselectDocumentTreeException e) {
- super.onCreate(savedInstanceState);
- final Intent intent = new Intent(this, DirectorySelectActivity.class);
- intent.putExtra(EXTRA_INITIAL_URI, e.getInitialUri());
- startActivity(intent);
- finish();
- return;
- }
- RetrofitFactory.setup(this);
- super.onCreate(savedInstanceState);
- binding = ActivityMainBinding.inflate(getLayoutInflater());
- setupCookie();
- if (settingsHelper.getBoolean(Constants.FLAG_SECURE))
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
- setContentView(binding.getRoot());
- final Toolbar toolbar = binding.toolbar;
- setSupportActionBar(toolbar);
- createNotificationChannels();
- try {
- final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.bottomNavView.getLayoutParams();
- //noinspection unchecked
- behavior = (HideBottomViewOnScrollBehavior) layoutParams.getBehavior();
- } catch (Exception e) {
- Log.e(TAG, "onCreate: ", e);
- }
- if (savedInstanceState == null) {
- setupBottomNavigationBar(true);
- }
- setupSuggestions();
- if (!BuildConfig.isPre) {
- final boolean checkUpdates = settingsHelper.getBoolean(Constants.CHECK_UPDATES);
- if (checkUpdates) FlavorTown.updateCheck(this);
- }
- FlavorTown.changelogCheck(this);
- new ViewModelProvider(this).get(AppStateViewModel.class); // Just initiate the App state here
- final Intent intent = getIntent();
- handleIntent(intent);
- if (isLoggedIn && settingsHelper.getBoolean(Constants.CHECK_ACTIVITY)) {
- bindActivityCheckerService();
- }
- getSupportFragmentManager().addOnBackStackChangedListener(this);
- // Initialise the internal map
- AppExecutors.getInstance().tasksThread().execute(() -> {
- EmojiParser.setup(this);
- EmojiVariantManager.getInstance();
- });
- initEmojiCompat();
- searchService = SearchService.getInstance();
- // initDmService();
- initDmUnreadCount();
- }
-
- private void setupCookie() {
- final String cookie = settingsHelper.getString(Constants.COOKIE);
- long userId = 0;
- String csrfToken = null;
- if (!TextUtils.isEmpty(cookie)) {
- userId = CookieUtils.getUserIdFromCookie(cookie);
- csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
- }
- if (TextUtils.isEmpty(cookie) || userId == 0 || TextUtils.isEmpty(csrfToken)) {
- isLoggedIn = false;
- return;
- }
- final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID);
- if (TextUtils.isEmpty(deviceUuid)) {
- settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString());
- }
- CookieUtils.setupCookies(cookie);
- isLoggedIn = true;
- }
-
- private void initDmService() {
- if (!isLoggedIn) return;
- final boolean enabled = settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH);
- if (!enabled) return;
- DMSyncAlarmReceiver.setAlarm(this);
- }
-
- private void initDmUnreadCount() {
- if (!isLoggedIn) return;
- final DirectInboxViewModel directInboxViewModel = new ViewModelProvider(this).get(DirectInboxViewModel.class);
- directInboxViewModel.getUnseenCount().observe(this, unseenCountResource -> {
- if (unseenCountResource == null) return;
- final Integer unseenCount = unseenCountResource.data;
- setNavBarDMUnreadCountBadge(unseenCount == null ? 0 : unseenCount);
- });
- }
-
- @Override
- public boolean onCreateOptionsMenu(final Menu menu) {
- getMenuInflater().inflate(R.menu.main_menu, menu);
- searchMenuItem = menu.findItem(R.id.search);
- if (showSearch && currentNavControllerLiveData != null) {
- final NavController navController = currentNavControllerLiveData.getValue();
- if (navController != null) {
- final NavDestination currentDestination = navController.getCurrentDestination();
- if (currentDestination != null) {
- final int destinationId = currentDestination.getId();
- showSearch = destinationId == R.id.profileFragment;
- }
- }
- }
- if (!showSearch) {
- searchMenuItem.setVisible(false);
- return true;
- }
- return setupSearchView();
- }
-
- @Override
- protected void onSaveInstanceState(@NonNull final Bundle outState) {
- outState.putString(FIRST_FRAGMENT_GRAPH_INDEX_KEY, String.valueOf(firstFragmentGraphIndex));
- if (binding != null) {
- outState.putString(LAST_SELECT_NAV_MENU_ID, String.valueOf(binding.bottomNavView.getSelectedItemId()));
- }
- super.onSaveInstanceState(outState);
- }
-
- @Override
- protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
- super.onRestoreInstanceState(savedInstanceState);
- final String key = (String) savedInstanceState.get(FIRST_FRAGMENT_GRAPH_INDEX_KEY);
- if (key != null) {
- try {
- firstFragmentGraphIndex = Integer.parseInt(key);
- } catch (NumberFormatException ignored) { }
- }
- final String lastSelected = (String) savedInstanceState.get(LAST_SELECT_NAV_MENU_ID);
- if (lastSelected != null) {
- try {
- lastSelectedNavMenuId = Integer.parseInt(lastSelected);
- } catch (NumberFormatException ignored) { }
- }
- setupBottomNavigationBar(false);
- }
-
- @Override
- public boolean onSupportNavigateUp() {
- if (currentNavControllerLiveData == null) return false;
- final NavController navController = currentNavControllerLiveData.getValue();
- if (navController == null) return false;
- return navController.navigateUp();
- }
-
- @Override
- protected void onNewIntent(final Intent intent) {
- super.onNewIntent(intent);
- handleIntent(intent);
- }
-
- @Override
- protected void onDestroy() {
- try {
- super.onDestroy();
- } catch (Exception e) {
- Log.e(TAG, "onDestroy: ", e);
- }
- unbindActivityCheckerService();
- try {
- RetrofitFactory.getInstance().destroy();
- } catch (Exception ignored) {}
- DownloadUtils.destroy();
- }
-
- @Override
- public void onBackPressed() {
- int currentNavControllerBackStack = 2;
- if (currentNavControllerLiveData != null) {
- final NavController navController = currentNavControllerLiveData.getValue();
- if (navController != null) {
- @SuppressLint("RestrictedApi") final Deque backStack = navController.getBackStack();
- currentNavControllerBackStack = backStack.size();
- }
- }
- if (isTaskRoot() && isBackStackEmpty && currentNavControllerBackStack == 2) {
- finishAfterTransition();
- return;
- }
- if (!isFinishing()) {
- try {
- super.onBackPressed();
- } catch (Exception e) {
- Log.e(TAG, "onBackPressed: ", e);
- finish();
- }
- }
- }
-
- @Override
- public void onBackStackChanged() {
- final int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount();
- isBackStackEmpty = backStackEntryCount == 0;
- }
-
- private void createNotificationChannels() {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
- final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext());
- notificationManager.createNotificationChannel(new NotificationChannel(Constants.DOWNLOAD_CHANNEL_ID,
- Constants.DOWNLOAD_CHANNEL_NAME,
- NotificationManager.IMPORTANCE_DEFAULT));
- notificationManager.createNotificationChannel(new NotificationChannel(Constants.ACTIVITY_CHANNEL_ID,
- Constants.ACTIVITY_CHANNEL_NAME,
- NotificationManager.IMPORTANCE_DEFAULT));
- notificationManager.createNotificationChannel(new NotificationChannel(Constants.DM_UNREAD_CHANNEL_ID,
- Constants.DM_UNREAD_CHANNEL_NAME,
- NotificationManager.IMPORTANCE_DEFAULT));
- final NotificationChannel silentNotificationChannel = new NotificationChannel(Constants.SILENT_NOTIFICATIONS_CHANNEL_ID,
- Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME,
- NotificationManager.IMPORTANCE_LOW);
- silentNotificationChannel.setSound(null, null);
- notificationManager.createNotificationChannel(silentNotificationChannel);
- }
-
- private void setupSuggestions() {
- suggestionsFetchHandler = new Handler();
- suggestionAdapter = new SuggestionsAdapter(this, (type, query) -> {
- if (searchMenuItem != null) searchMenuItem.collapseActionView();
- if (searchView != null && !searchView.isIconified()) searchView.setIconified(true);
- if (currentNavControllerLiveData == null) return;
- final NavController navController = currentNavControllerLiveData.getValue();
- if (navController == null) return;
- final Bundle bundle = new Bundle();
- switch (type) {
- case TYPE_LOCATION:
- bundle.putLong("locationId", Long.parseLong(query));
- navController.navigate(R.id.action_global_locationFragment, bundle);
- break;
- case TYPE_HASHTAG:
- bundle.putString("hashtag", query);
- navController.navigate(R.id.action_global_hashTagFragment, bundle);
- break;
- case TYPE_USER:
- bundle.putString("username", query);
- navController.navigate(R.id.action_global_profileFragment, bundle);
- break;
- }
- });
- }
-
- private boolean setupSearchView() {
- final View actionView = searchMenuItem.getActionView();
- if (!(actionView instanceof SearchView)) return false;
- searchView = (SearchView) actionView;
- searchView.setSuggestionsAdapter(suggestionAdapter);
- searchView.setMaxWidth(Integer.MAX_VALUE);
- final View searchText = searchView.findViewById(R.id.search_src_text);
- if (searchText instanceof AutoCompleteTextView) {
- searchAutoComplete = (AutoCompleteTextView) searchText;
- }
- searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
- private boolean searchUser;
- private boolean searchHash;
- private Call prevSuggestionAsync;
- private final String[] COLUMNS = {
- BaseColumns._ID,
- Constants.EXTRAS_USERNAME,
- Constants.EXTRAS_NAME,
- Constants.EXTRAS_TYPE,
- "query",
- "pfp",
- "verified"
- };
- private String currentSearchQuery;
-
- private final Callback cb = new Callback() {
- @Override
- public void onResponse(@NonNull final Call call,
- @NonNull final Response response) {
- final MatrixCursor cursor;
- final SearchResponse body = response.body();
- if (body == null) {
- cursor = null;
- return;
- }
- final List result = new ArrayList<>();
- if (isLoggedIn) {
- if (body.getList() != null) {
- result.addAll(searchHash ? body.getList()
- .stream()
- .filter(i -> i.getUser() == null)
- .collect(Collectors.toList())
- : body.getList());
- }
- } else {
- if (body.getUsers() != null && !searchHash) result.addAll(body.getUsers());
- if (body.getHashtags() != null) result.addAll(body.getHashtags());
- if (body.getPlaces() != null) result.addAll(body.getPlaces());
- }
- cursor = new MatrixCursor(COLUMNS, 0);
- for (int i = 0; i < result.size(); i++) {
- final SearchItem suggestionModel = result.get(i);
- if (suggestionModel != null) {
- Object[] objects = null;
- if (suggestionModel.getUser() != null)
- objects = new Object[]{
- suggestionModel.getPosition(),
- suggestionModel.getUser().getUsername(),
- suggestionModel.getUser().getFullName(),
- SuggestionType.TYPE_USER,
- suggestionModel.getUser().getUsername(),
- suggestionModel.getUser().getProfilePicUrl(),
- suggestionModel.getUser().isVerified()};
- else if (suggestionModel.getHashtag() != null)
- objects = new Object[]{
- suggestionModel.getPosition(),
- suggestionModel.getHashtag().getName(),
- suggestionModel.getHashtag().getSubtitle(),
- SuggestionType.TYPE_HASHTAG,
- suggestionModel.getHashtag().getName(),
- "res:/" + R.drawable.ic_hashtag,
- false};
- else if (suggestionModel.getPlace() != null)
- objects = new Object[]{
- suggestionModel.getPosition(),
- suggestionModel.getPlace().getTitle(),
- suggestionModel.getPlace().getSubtitle(),
- SuggestionType.TYPE_LOCATION,
- suggestionModel.getPlace().getLocation().getPk(),
- "res:/" + R.drawable.ic_location,
- false};
- cursor.addRow(objects);
- }
- }
- suggestionAdapter.changeCursor(cursor);
- }
-
- @Override
- public void onFailure(@NonNull final Call call,
- @NonNull Throwable t) {
- if (!call.isCanceled()) {
- Log.e(TAG, "Exception on search:", t);
- }
- }
- };
-
- private final Runnable runnable = () -> {
- cancelSuggestionsAsync();
- if (TextUtils.isEmpty(currentSearchQuery)) {
- suggestionAdapter.changeCursor(null);
- return;
- }
- searchUser = currentSearchQuery.charAt(0) == '@';
- searchHash = currentSearchQuery.charAt(0) == '#';
- if (currentSearchQuery.length() == 1 && (searchHash || searchUser)) {
- if (searchAutoComplete != null) {
- searchAutoComplete.setThreshold(2);
- }
- } else {
- if (searchAutoComplete != null) {
- searchAutoComplete.setThreshold(1);
- }
- prevSuggestionAsync = searchService.search(isLoggedIn,
- searchUser || searchHash ? currentSearchQuery.substring(1)
- : currentSearchQuery,
- searchUser ? "user" : (searchHash ? "hashtag" : "blended"));
- suggestionAdapter.changeCursor(null);
- prevSuggestionAsync.enqueue(cb);
- }
- };
-
- private void cancelSuggestionsAsync() {
- if (prevSuggestionAsync != null)
- try {
- prevSuggestionAsync.cancel();
- } catch (final Exception ignored) {}
- }
-
- @Override
- public boolean onQueryTextSubmit(final String query) {
- return onQueryTextChange(query);
- }
-
- @Override
- public boolean onQueryTextChange(final String query) {
- suggestionsFetchHandler.removeCallbacks(runnable);
- currentSearchQuery = query;
- suggestionsFetchHandler.postDelayed(runnable, 800);
- return true;
- }
- });
- return true;
- }
-
- private void setupBottomNavigationBar(final boolean setDefaultTabFromSettings) {
- currentTabs = !isLoggedIn ? setupAnonBottomNav() : setupMainBottomNav();
- final List mainNavList = currentTabs.stream()
- .map(Tab::getNavigationResId)
- .collect(Collectors.toList());
- showBottomViewDestinations = currentTabs.stream()
- .map(Tab::getStartDestinationFragmentId)
- .collect(Collectors.toList());
- if (setDefaultTabFromSettings) {
- setSelectedTab(currentTabs);
- } else {
- binding.bottomNavView.setSelectedItemId(lastSelectedNavMenuId);
- }
- final LiveData navControllerLiveData = setupWithNavController(
- binding.bottomNavView,
- mainNavList,
- getSupportFragmentManager(),
- R.id.main_nav_host,
- getIntent(),
- firstFragmentGraphIndex);
- navControllerLiveData.observe(this, navController -> setupNavigation(binding.toolbar, navController));
- currentNavControllerLiveData = navControllerLiveData;
- binding.bottomNavView.setOnNavigationItemReselectedListener(item -> {
- // Log.d(TAG, "setupBottomNavigationBar: item: " + item);
- final Fragment navHostFragment = getSupportFragmentManager().findFragmentById(R.id.main_nav_host);
- if (navHostFragment != null) {
- final Fragment fragment = navHostFragment.getChildFragmentManager().getPrimaryNavigationFragment();
- if (fragment instanceof FeedFragment) {
- ((FeedFragment) fragment).scrollToTop();
- }
- }
- });
- }
-
- private void setSelectedTab(final List tabs) {
- final String defaultTabResNameString = settingsHelper.getString(Constants.DEFAULT_TAB);
- try {
- int navId = 0;
- if (!TextUtils.isEmpty(defaultTabResNameString)) {
- navId = getResources().getIdentifier(defaultTabResNameString, "navigation", getPackageName());
- }
- final int navGraph = isLoggedIn ? R.navigation.feed_nav_graph
- : R.navigation.profile_nav_graph;
- final int defaultNavId = navId <= 0 ? navGraph : navId;
- int index = Iterators.indexOf(tabs.iterator(), tab -> {
- if (tab == null) return false;
- return tab.getNavigationResId() == defaultNavId;
- });
- if (index < 0 || index >= tabs.size()) index = 0;
- firstFragmentGraphIndex = index;
- setBottomNavSelectedTab(tabs.get(index));
- } catch (Exception e) {
- Log.e(TAG, "Error parsing id", e);
- }
- }
-
- private List setupAnonBottomNav() {
- final int selectedItemId = binding.bottomNavView.getSelectedItemId();
- final Tab profileTab = new Tab(R.drawable.ic_person_24,
- getString(R.string.profile),
- false,
- "profile_nav_graph",
- R.navigation.profile_nav_graph,
- R.id.profile_nav_graph,
- R.id.profileFragment);
- final Tab moreTab = new Tab(R.drawable.ic_more_horiz_24,
- getString(R.string.more),
- false,
- "more_nav_graph",
- R.navigation.more_nav_graph,
- R.id.more_nav_graph,
- R.id.morePreferencesFragment);
- final Menu menu = binding.bottomNavView.getMenu();
- menu.clear();
- menu.add(0, profileTab.getNavigationRootId(), 0, profileTab.getTitle()).setIcon(profileTab.getIconResId());
- menu.add(0, moreTab.getNavigationRootId(), 0, moreTab.getTitle()).setIcon(moreTab.getIconResId());
- if (selectedItemId != R.id.profile_nav_graph && selectedItemId != R.id.more_nav_graph) {
- setBottomNavSelectedTab(profileTab);
- }
- return ImmutableList.of(profileTab, moreTab);
- }
-
- private List setupMainBottomNav() {
- final Menu menu = binding.bottomNavView.getMenu();
- menu.clear();
- final List navTabList = Utils.getNavTabList(this).first;
- for (final Tab tab : navTabList) {
- menu.add(0, tab.getNavigationRootId(), 0, tab.getTitle()).setIcon(tab.getIconResId());
- }
- return navTabList;
- }
-
- private void setBottomNavSelectedTab(@NonNull final Tab tab) {
- binding.bottomNavView.setSelectedItemId(tab.getNavigationRootId());
- }
-
- private void setBottomNavSelectedTab(@SuppressWarnings("SameParameterValue") @IdRes final int navGraphRootId) {
- binding.bottomNavView.setSelectedItemId(navGraphRootId);
- }
-
- // @NonNull
- // private List getMainNavList(final int main_nav_ids) {
- // final TypedArray navIds = getResources().obtainTypedArray(main_nav_ids);
- // final List mainNavList = new ArrayList<>(navIds.length());
- // final int length = navIds.length();
- // for (int i = 0; i < length; i++) {
- // final int resourceId = navIds.getResourceId(i, -1);
- // if (resourceId < 0) continue;
- // mainNavList.add(resourceId);
- // }
- // navIds.recycle();
- // return mainNavList;
- // }
-
- private void setupNavigation(final Toolbar toolbar, final NavController navController) {
- if (navController == null) return;
- NavigationUI.setupWithNavController(toolbar, navController);
- navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
- if (destination.getId() == R.id.directMessagesThreadFragment && arguments != null) {
- // Set the thread title earlier for better ux
- final String title = arguments.getString("title");
- final ActionBar actionBar = getSupportActionBar();
- if (actionBar != null && !TextUtils.isEmpty(title)) {
- actionBar.setTitle(title);
- }
- }
- // below is a hack to check if we are at the end of the current stack, to setup the search view
- binding.appBarLayout.setExpanded(true, true);
- final int destinationId = destination.getId();
- @SuppressLint("RestrictedApi") final Deque backStack = navController.getBackStack();
- setupMenu(backStack.size(), destinationId);
- final boolean contains = showBottomViewDestinations.contains(destinationId);
- binding.bottomNavView.setVisibility(contains ? View.VISIBLE : View.GONE);
- if (contains && behavior != null) {
- behavior.slideUp(binding.bottomNavView);
- }
-
- // explicitly hide keyboard when we navigate
- final View view = getCurrentFocus();
- Utils.hideKeyboard(view);
- });
- }
-
- private void setupMenu(final int backStackSize, final int destinationId) {
- if (searchMenuItem == null) return;
- if (backStackSize >= 2 && destinationId == R.id.profileFragment) {
- showSearch = true;
- searchMenuItem.setVisible(true);
- return;
- }
- showSearch = false;
- searchMenuItem.setVisible(false);
- }
-
- private void setScrollingBehaviour() {
- final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.mainNavHost.getLayoutParams();
- layoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
- binding.mainNavHost.requestLayout();
- }
-
- private void removeScrollingBehaviour() {
- final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.mainNavHost.getLayoutParams();
- layoutParams.setBehavior(null);
- binding.mainNavHost.requestLayout();
- }
-
- private void handleIntent(final Intent intent) {
- if (intent == null) return;
- final String action = intent.getAction();
- final String type = intent.getType();
- // Log.d(TAG, action + " " + type);
- if (Intent.ACTION_MAIN.equals(action)) return;
- if (Constants.ACTION_SHOW_ACTIVITY.equals(action)) {
- showActivityView();
- return;
- }
- if (Constants.ACTION_SHOW_DM_THREAD.equals(action)) {
- showThread(intent);
- return;
- }
- if (Intent.ACTION_SEND.equals(action) && type != null) {
- if (type.equals("text/plain")) {
- handleUrl(intent.getStringExtra(Intent.EXTRA_TEXT));
- }
- return;
- }
- if (Intent.ACTION_VIEW.equals(action)) {
- final Uri data = intent.getData();
- if (data == null) return;
- handleUrl(data.toString());
- }
- }
-
- private void showThread(@NonNull final Intent intent) {
- final String threadId = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_ID);
- final String threadTitle = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_TITLE);
- navigateToThread(threadId, threadTitle);
- }
-
- public void navigateToThread(final String threadId, final String threadTitle) {
- if (threadId == null || threadTitle == null) return;
- currentNavControllerLiveData.observe(this, new Observer() {
- @Override
- public void onChanged(final NavController navController) {
- if (navController == null) return;
- if (navController.getGraph().getId() != R.id.direct_messages_nav_graph) return;
- try {
- final NavDestination currentDestination = navController.getCurrentDestination();
- if (currentDestination != null && currentDestination.getId() == R.id.directMessagesInboxFragment) {
- // if we are already on the inbox page, navigate to the thread
- // need handler.post() to wait for the fragment manager to be ready to navigate
- new Handler().post(() -> {
- final DirectMessageInboxFragmentDirections.ActionInboxToThread action = DirectMessageInboxFragmentDirections
- .actionInboxToThread(threadId, threadTitle);
- navController.navigate(action);
- });
- return;
- }
- // add a destination change listener to navigate to thread once we are on the inbox page
- navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {
- @Override
- public void onDestinationChanged(@NonNull final NavController controller,
- @NonNull final NavDestination destination,
- @Nullable final Bundle arguments) {
- if (destination.getId() == R.id.directMessagesInboxFragment) {
- final DirectMessageInboxFragmentDirections.ActionInboxToThread action = DirectMessageInboxFragmentDirections
- .actionInboxToThread(threadId, threadTitle);
- controller.navigate(action);
- controller.removeOnDestinationChangedListener(this);
- }
- }
- });
- // pop back stack until we reach the inbox page
- navController.popBackStack(R.id.directMessagesInboxFragment, false);
- } finally {
- currentNavControllerLiveData.removeObserver(this);
- }
- }
- });
- final int selectedItemId = binding.bottomNavView.getSelectedItemId();
- if (selectedItemId != R.navigation.direct_messages_nav_graph) {
- setBottomNavSelectedTab(R.id.direct_messages_nav_graph);
- }
- }
-
- private void handleUrl(final String url) {
- if (url == null) return;
- // Log.d(TAG, url);
- final IntentModel intentModel = IntentUtils.parseUrl(url);
- if (intentModel == null) return;
- showView(intentModel);
- }
-
- private void showView(final IntentModel intentModel) {
- switch (intentModel.getType()) {
- case USERNAME:
- showProfileView(intentModel);
- break;
- case POST:
- showPostView(intentModel);
- break;
- case LOCATION:
- showLocationView(intentModel);
- break;
- case HASHTAG:
- showHashtagView(intentModel);
- break;
- case UNKNOWN:
- default:
- Log.w(TAG, "Unknown model type received!");
- }
- }
-
- private void showProfileView(@NonNull final IntentModel intentModel) {
- final String username = intentModel.getText();
- // Log.d(TAG, "username: " + username);
- if (currentNavControllerLiveData == null) return;
- final NavController navController = currentNavControllerLiveData.getValue();
- if (navController == null) return;
- final Bundle bundle = new Bundle();
- bundle.putString("username", "@" + username);
- navController.navigate(R.id.action_global_profileFragment, bundle);
- }
-
- private void showPostView(@NonNull final IntentModel intentModel) {
- final String shortCode = intentModel.getText();
- // Log.d(TAG, "shortCode: " + shortCode);
- final AlertDialog alertDialog = new AlertDialog.Builder(this)
- .setCancelable(false)
- .setView(R.layout.dialog_opening_post)
- .create();
- alertDialog.show();
- new PostFetcher(shortCode, feedModel -> {
- if (feedModel != null) {
- final PostViewV2Fragment fragment = PostViewV2Fragment
- .builder(feedModel)
- .build();
- fragment.setOnShowListener(dialog -> alertDialog.dismiss());
- fragment.show(getSupportFragmentManager(), "post_view");
- return;
- }
- Toast.makeText(getApplicationContext(), R.string.post_not_found, Toast.LENGTH_SHORT).show();
- alertDialog.dismiss();
- }).execute();
- }
-
- private void showLocationView(@NonNull final IntentModel intentModel) {
- final String locationId = intentModel.getText();
- // Log.d(TAG, "locationId: " + locationId);
- if (currentNavControllerLiveData == null) return;
- final NavController navController = currentNavControllerLiveData.getValue();
- if (navController == null) return;
- final Bundle bundle = new Bundle();
- bundle.putLong("locationId", Long.parseLong(locationId));
- navController.navigate(R.id.action_global_locationFragment, bundle);
- }
-
- private void showHashtagView(@NonNull final IntentModel intentModel) {
- final String hashtag = intentModel.getText();
- // Log.d(TAG, "hashtag: " + hashtag);
- if (currentNavControllerLiveData == null) return;
- final NavController navController = currentNavControllerLiveData.getValue();
- if (navController == null) return;
- final Bundle bundle = new Bundle();
- bundle.putString("hashtag", hashtag);
- navController.navigate(R.id.action_global_hashTagFragment, bundle);
- }
-
- private void showActivityView() {
- if (currentNavControllerLiveData == null) return;
- final NavController navController = currentNavControllerLiveData.getValue();
- if (navController == null) return;
- final Bundle bundle = new Bundle();
- bundle.putString("type", "notif");
- navController.navigate(R.id.action_global_notificationsViewerFragment, bundle);
- }
-
- private void bindActivityCheckerService() {
- bindService(new Intent(this, ActivityCheckerService.class), serviceConnection, Context.BIND_AUTO_CREATE);
- isActivityCheckerServiceBound = true;
- }
-
- private void unbindActivityCheckerService() {
- if (!isActivityCheckerServiceBound) return;
- unbindService(serviceConnection);
- isActivityCheckerServiceBound = false;
- }
-
- @NonNull
- public BottomNavigationView getBottomNavView() {
- return binding.bottomNavView;
- }
-
- public void setCollapsingView(@NonNull final View view) {
- binding.collapsingToolbarLayout.addView(view, 0);
- }
-
- public void removeCollapsingView(@NonNull final View view) {
- binding.collapsingToolbarLayout.removeView(view);
- }
-
- public void setToolbar(final Toolbar toolbar) {
- binding.appBarLayout.setVisibility(View.GONE);
- removeScrollingBehaviour();
- setSupportActionBar(toolbar);
- if (currentNavControllerLiveData == null) return;
- setupNavigation(toolbar, currentNavControllerLiveData.getValue());
- }
-
- public void resetToolbar() {
- binding.appBarLayout.setVisibility(View.VISIBLE);
- setScrollingBehaviour();
- setSupportActionBar(binding.toolbar);
- if (currentNavControllerLiveData == null) return;
- setupNavigation(binding.toolbar, currentNavControllerLiveData.getValue());
- }
-
- public CollapsingToolbarLayout getCollapsingToolbarView() {
- return binding.collapsingToolbarLayout;
- }
-
- public AppBarLayout getAppbarLayout() {
- return binding.appBarLayout;
- }
-
- public void removeLayoutTransition() {
- binding.getRoot().setLayoutTransition(null);
- }
-
- public void setLayoutTransition() {
- binding.getRoot().setLayoutTransition(new LayoutTransition());
- }
-
- private void initEmojiCompat() {
- // Use a downloadable font for EmojiCompat
- final FontRequest fontRequest = new FontRequest(
- "com.google.android.gms.fonts",
- "com.google.android.gms",
- "Noto Color Emoji Compat",
- R.array.com_google_android_gms_fonts_certs);
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(getApplicationContext(), fontRequest);
- config.setReplaceAll(true)
- // .setUseEmojiAsDefaultStyle(true)
- .registerInitCallback(new EmojiCompat.InitCallback() {
- @Override
- public void onInitialized() {
- Log.i(TAG, "EmojiCompat initialized");
- }
-
- @Override
- public void onFailed(@Nullable Throwable throwable) {
- Log.e(TAG, "EmojiCompat initialization failed", throwable);
- }
- });
- EmojiCompat.init(config);
- }
-
- public Toolbar getToolbar() {
- return binding.toolbar;
- }
-
- public View getRootView() {
- return binding.getRoot();
- }
-
- public List getCurrentTabs() {
- return currentTabs;
- }
-
- // public boolean isNavRootInCurrentTabs(@IdRes final int navRootId) {
- // return showBottomViewDestinations.stream().anyMatch(id -> id == navRootId);
- // }
-
- private void setNavBarDMUnreadCountBadge(final int unseenCount) {
- final BadgeDrawable badge = binding.bottomNavView.getOrCreateBadge(R.id.direct_messages_nav_graph);
- if (badge == null) return;
- if (unseenCount == 0) {
- badge.setVisible(false);
- badge.clearNumber();
- return;
- }
- if (badge.getVerticalOffset() != 10) {
- badge.setVerticalOffset(10);
- }
- badge.setNumber(unseenCount);
- badge.setVisible(true);
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.kt b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt
new file mode 100644
index 00000000..90be552a
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt
@@ -0,0 +1,821 @@
+package awais.instagrabber.activities
+
+import android.animation.LayoutTransition
+import android.annotation.SuppressLint
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.ComponentName
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.*
+import android.text.Editable
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.annotation.IdRes
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.widget.Toolbar
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.provider.FontRequest
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.emoji.text.EmojiCompat
+import androidx.emoji.text.EmojiCompat.InitCallback
+import androidx.emoji.text.FontRequestEmojiCompatConfig
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.NavController
+import androidx.navigation.NavController.OnDestinationChangedListener
+import androidx.navigation.NavDestination
+import androidx.navigation.ui.NavigationUI
+import awais.instagrabber.BuildConfig
+import awais.instagrabber.R
+import awais.instagrabber.customviews.emoji.EmojiVariantManager
+import awais.instagrabber.customviews.helpers.RootViewDeferringInsetsCallback
+import awais.instagrabber.customviews.helpers.TextWatcherAdapter
+import awais.instagrabber.databinding.ActivityMainBinding
+import awais.instagrabber.fragments.PostViewV2Fragment
+import awais.instagrabber.fragments.directmessages.DirectMessageInboxFragmentDirections
+import awais.instagrabber.fragments.settings.PreferenceKeys
+import awais.instagrabber.models.IntentModel
+import awais.instagrabber.models.Resource
+import awais.instagrabber.models.Tab
+import awais.instagrabber.models.enums.IntentModelType
+import awais.instagrabber.services.ActivityCheckerService
+import awais.instagrabber.services.DMSyncAlarmReceiver
+import awais.instagrabber.utils.*
+import awais.instagrabber.utils.AppExecutors.tasksThread
+import awais.instagrabber.utils.TextUtils.isEmpty
+import awais.instagrabber.utils.TextUtils.shortcodeToId
+import awais.instagrabber.utils.emoji.EmojiParser
+import awais.instagrabber.viewmodels.AppStateViewModel
+import awais.instagrabber.viewmodels.DirectInboxViewModel
+import awais.instagrabber.webservices.GraphQLRepository
+import awais.instagrabber.webservices.MediaRepository
+import com.google.android.material.appbar.AppBarLayout
+import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
+import com.google.android.material.appbar.CollapsingToolbarLayout
+import com.google.android.material.bottomnavigation.BottomNavigationView
+import com.google.android.material.textfield.TextInputLayout
+import com.google.common.collect.ImmutableList
+import com.google.common.collect.Iterators
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.util.*
+import java.util.stream.Collectors
+
+class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedListener {
+ private lateinit var binding: ActivityMainBinding
+
+ private var currentNavControllerLiveData: LiveData? = null
+ private var searchMenuItem: MenuItem? = null
+ private var firstFragmentGraphIndex = 0
+ private var lastSelectedNavMenuId = 0
+ private var isActivityCheckerServiceBound = false
+ private var isBackStackEmpty = false
+ private var isLoggedIn = false
+ private var deviceUuid: String? = null
+ private var csrfToken: String? = null
+ private var userId: Long = 0
+
+ // private var behavior: HideBottomViewOnScrollBehavior? = null
+ var currentTabs: List = emptyList()
+ private set
+ private var showBottomViewDestinations: List = emptyList()
+
+ private val serviceConnection: ServiceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName, service: IBinder) {
+ // final ActivityCheckerService.LocalBinder binder = (ActivityCheckerService.LocalBinder) service;
+ // final ActivityCheckerService activityCheckerService = binder.getService();
+ isActivityCheckerServiceBound = true
+ }
+
+ override fun onServiceDisconnected(name: ComponentName) {
+ isActivityCheckerServiceBound = false
+ }
+ }
+ private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() }
+ private val graphQLRepository: GraphQLRepository by lazy { GraphQLRepository.getInstance() }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ instance = this
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setupCookie()
+ if (Utils.settingsHelper.getBoolean(PreferenceKeys.FLAG_SECURE)) {
+ window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
+ }
+ setContentView(binding.root)
+ setSupportActionBar(binding.toolbar)
+ setupInsetsCallback()
+ createNotificationChannels()
+ // try {
+ // val layoutParams = binding.bottomNavView.layoutParams as CoordinatorLayout.LayoutParams
+ // @Suppress("UNCHECKED_CAST")
+ // behavior = layoutParams.behavior as HideBottomViewOnScrollBehavior
+ // } catch (e: Exception) {
+ // Log.e(TAG, "onCreate: ", e)
+ // }
+ if (savedInstanceState == null) {
+ setupBottomNavigationBar(true)
+ }
+ if (!BuildConfig.isPre) {
+ val checkUpdates = Utils.settingsHelper.getBoolean(PreferenceKeys.CHECK_UPDATES)
+ if (checkUpdates) FlavorTown.updateCheck(this)
+ }
+ FlavorTown.changelogCheck(this)
+ ViewModelProvider(this).get(AppStateViewModel::class.java) // Just initiate the App state here
+ handleIntent(intent)
+ if (isLoggedIn && Utils.settingsHelper.getBoolean(PreferenceKeys.CHECK_ACTIVITY)) {
+ bindActivityCheckerService()
+ }
+ supportFragmentManager.addOnBackStackChangedListener(this)
+ // Initialise the internal map
+ tasksThread.execute {
+ EmojiParser.getInstance(this)
+ EmojiVariantManager.getInstance()
+ }
+ initEmojiCompat()
+ // initDmService();
+ initDmUnreadCount()
+ initSearchInput()
+ }
+
+ private fun setupInsetsCallback() {
+ val deferringInsetsCallback = RootViewDeferringInsetsCallback(
+ WindowInsetsCompat.Type.systemBars(),
+ WindowInsetsCompat.Type.ime()
+ )
+ ViewCompat.setWindowInsetsAnimationCallback(binding.root, deferringInsetsCallback)
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root, deferringInsetsCallback)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ }
+
+ private fun setupCookie() {
+ val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
+ userId = 0
+ csrfToken = null
+ if (cookie.isNotBlank()) {
+ userId = getUserIdFromCookie(cookie)
+ csrfToken = getCsrfTokenFromCookie(cookie)
+ }
+ if (cookie.isBlank() || userId == 0L || csrfToken.isNullOrBlank()) {
+ isLoggedIn = false
+ return
+ }
+ deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
+ if (isEmpty(deviceUuid)) {
+ Utils.settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString())
+ }
+ setupCookies(cookie)
+ isLoggedIn = true
+ }
+
+ @Suppress("unused")
+ private fun initDmService() {
+ if (!isLoggedIn) return
+ val enabled = Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH)
+ if (!enabled) return
+ DMSyncAlarmReceiver.setAlarm(this)
+ }
+
+ private fun initDmUnreadCount() {
+ if (!isLoggedIn) return
+ val directInboxViewModel = ViewModelProvider(this).get(DirectInboxViewModel::class.java)
+ directInboxViewModel.unseenCount.observe(this, { unseenCountResource: Resource? ->
+ if (unseenCountResource == null) return@observe
+ val unseenCount = unseenCountResource.data
+ setNavBarDMUnreadCountBadge(unseenCount ?: 0)
+ })
+ }
+
+ private fun initSearchInput() {
+ binding.searchInputLayout.setEndIconOnClickListener {
+ val editText = binding.searchInputLayout.editText ?: return@setEndIconOnClickListener
+ editText.setText("")
+ }
+ binding.searchInputLayout.addOnEditTextAttachedListener { textInputLayout: TextInputLayout ->
+ textInputLayout.isEndIconVisible = false
+ val editText = textInputLayout.editText ?: return@addOnEditTextAttachedListener
+ editText.addTextChangedListener(object : TextWatcherAdapter() {
+ override fun afterTextChanged(s: Editable) {
+ binding.searchInputLayout.isEndIconVisible = !isEmpty(s)
+ }
+ })
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.main_menu, menu)
+ searchMenuItem = menu.findItem(R.id.search)
+ val navController = currentNavControllerLiveData?.value
+ if (navController != null) {
+ val currentDestination = navController.currentDestination
+ if (currentDestination != null) {
+ @SuppressLint("RestrictedApi") val backStack = navController.backStack
+ setupMenu(backStack.size, currentDestination.id)
+ }
+ }
+ // if (binding.searchInputLayout.getVisibility() == View.VISIBLE) {
+ // searchMenuItem.setVisible(false).setEnabled(false);
+ // return true;
+ // }
+ // searchMenuItem.setVisible(true).setEnabled(true);
+ // if (showSearch && currentNavControllerLiveData != null) {
+ // final NavController navController = currentNavControllerLiveData.getValue();
+ // if (navController != null) {
+ // final NavDestination currentDestination = navController.getCurrentDestination();
+ // if (currentDestination != null) {
+ // final int destinationId = currentDestination.getId();
+ // showSearch = destinationId == R.id.profileFragment;
+ // }
+ // }
+ // }
+ // if (!showSearch) {
+ // searchMenuItem.setVisible(false);
+ // return true;
+ // }
+ // return setupSearchView();
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == R.id.search) {
+ val navController = currentNavControllerLiveData?.value ?: return false
+ try {
+ navController.navigate(R.id.action_global_search)
+ return true
+ } catch (e: Exception) {
+ Log.e(TAG, "onOptionsItemSelected: ", e)
+ }
+ return false
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putString(FIRST_FRAGMENT_GRAPH_INDEX_KEY, firstFragmentGraphIndex.toString())
+ outState.putString(LAST_SELECT_NAV_MENU_ID, binding.bottomNavView.selectedItemId.toString())
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+ val key = savedInstanceState[FIRST_FRAGMENT_GRAPH_INDEX_KEY] as String?
+ if (key != null) {
+ try {
+ firstFragmentGraphIndex = key.toInt()
+ } catch (ignored: NumberFormatException) {
+ }
+ }
+ val lastSelected = savedInstanceState[LAST_SELECT_NAV_MENU_ID] as String?
+ if (lastSelected != null) {
+ try {
+ lastSelectedNavMenuId = lastSelected.toInt()
+ } catch (ignored: NumberFormatException) {
+ }
+ }
+ setupBottomNavigationBar(false)
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ if (currentNavControllerLiveData == null) return false
+ val navController = currentNavControllerLiveData?.value ?: return false
+ return navController.navigateUp()
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ handleIntent(intent)
+ }
+
+ override fun onDestroy() {
+ try {
+ super.onDestroy()
+ } catch (e: Exception) {
+ Log.e(TAG, "onDestroy: ", e)
+ }
+ unbindActivityCheckerService()
+ // try {
+ // RetrofitFactory.getInstance().destroy()
+ // } catch (e: Exception) {
+ // Log.e(TAG, "onDestroy: ", e)
+ // }
+ instance = null
+ }
+
+ override fun onBackPressed() {
+ var currentNavControllerBackStack = 2
+ currentNavControllerLiveData?.let {
+ val navController = it.value
+ if (navController != null) {
+ @SuppressLint("RestrictedApi") val backStack = navController.backStack
+ currentNavControllerBackStack = backStack.size
+ }
+ }
+ if (isTaskRoot && isBackStackEmpty && currentNavControllerBackStack == 2) {
+ finishAfterTransition()
+ return
+ }
+ if (!isFinishing) {
+ try {
+ super.onBackPressed()
+ } catch (e: Exception) {
+ Log.e(TAG, "onBackPressed: ", e)
+ finish()
+ }
+ }
+ }
+
+ override fun onBackStackChanged() {
+ val backStackEntryCount = supportFragmentManager.backStackEntryCount
+ isBackStackEmpty = backStackEntryCount == 0
+ }
+
+ private fun createNotificationChannels() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
+ val notificationManager = NotificationManagerCompat.from(applicationContext)
+ notificationManager.createNotificationChannel(NotificationChannel(
+ Constants.DOWNLOAD_CHANNEL_ID,
+ Constants.DOWNLOAD_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_DEFAULT
+ ))
+ notificationManager.createNotificationChannel(NotificationChannel(
+ Constants.ACTIVITY_CHANNEL_ID,
+ Constants.ACTIVITY_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_DEFAULT
+ ))
+ notificationManager.createNotificationChannel(NotificationChannel(
+ Constants.DM_UNREAD_CHANNEL_ID,
+ Constants.DM_UNREAD_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_DEFAULT
+ ))
+ val silentNotificationChannel = NotificationChannel(
+ Constants.SILENT_NOTIFICATIONS_CHANNEL_ID,
+ Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_LOW
+ )
+ silentNotificationChannel.setSound(null, null)
+ notificationManager.createNotificationChannel(silentNotificationChannel)
+ }
+
+ private fun setupBottomNavigationBar(setDefaultTabFromSettings: Boolean) {
+ currentTabs = if (!isLoggedIn) setupAnonBottomNav() else setupMainBottomNav()
+ val mainNavList = currentTabs.stream()
+ .map(Tab::navigationResId)
+ .collect(Collectors.toList())
+ showBottomViewDestinations = currentTabs.asSequence().map {
+ it.startDestinationFragmentId
+ }.toMutableList().apply {
+ add(R.id.postViewFragment)
+ add(R.id.favoritesFragment)
+ }
+ if (setDefaultTabFromSettings) {
+ setSelectedTab(currentTabs)
+ } else {
+ binding.bottomNavView.selectedItemId = lastSelectedNavMenuId
+ }
+ val navControllerLiveData = NavigationExtensions.setupWithNavController(
+ binding.bottomNavView,
+ mainNavList,
+ supportFragmentManager,
+ R.id.main_nav_host,
+ intent,
+ firstFragmentGraphIndex)
+ navControllerLiveData.observe(this, { navController: NavController? -> setupNavigation(binding.toolbar, navController) })
+ currentNavControllerLiveData = navControllerLiveData
+ }
+
+ private fun setSelectedTab(tabs: List) {
+ val defaultTabResNameString = Utils.settingsHelper.getString(Constants.DEFAULT_TAB)
+ try {
+ var navId = 0
+ if (!isEmpty(defaultTabResNameString)) {
+ navId = resources.getIdentifier(defaultTabResNameString, "navigation", packageName)
+ }
+ val navGraph = if (isLoggedIn) R.navigation.feed_nav_graph else R.navigation.profile_nav_graph
+ val defaultNavId = if (navId <= 0) navGraph else navId
+ var index = Iterators.indexOf(tabs.iterator()) { tab: Tab? ->
+ if (tab == null) return@indexOf false
+ tab.navigationResId == defaultNavId
+ }
+ if (index < 0 || index >= tabs.size) index = 0
+ firstFragmentGraphIndex = index
+ setBottomNavSelectedTab(tabs[index])
+ } catch (e: Exception) {
+ Log.e(TAG, "Error parsing id", e)
+ }
+ }
+
+ private fun setupAnonBottomNav(): List {
+ val selectedItemId = binding.bottomNavView.selectedItemId
+ val favoriteTab = Tab(R.drawable.ic_star_24,
+ getString(R.string.title_favorites),
+ false,
+ "favorites_nav_graph",
+ R.navigation.favorites_nav_graph,
+ R.id.favorites_nav_graph,
+ R.id.favoritesFragment)
+ val profileTab = Tab(R.drawable.ic_person_24,
+ getString(R.string.profile),
+ false,
+ "profile_nav_graph",
+ R.navigation.profile_nav_graph,
+ R.id.profile_nav_graph,
+ R.id.profileFragment)
+ val moreTab = Tab(R.drawable.ic_more_horiz_24,
+ getString(R.string.more),
+ false,
+ "more_nav_graph",
+ R.navigation.more_nav_graph,
+ R.id.more_nav_graph,
+ R.id.morePreferencesFragment)
+ val menu = binding.bottomNavView.menu
+ menu.clear()
+ menu.add(0, favoriteTab.navigationRootId, 0, favoriteTab.title).setIcon(favoriteTab.iconResId)
+ menu.add(0, profileTab.navigationRootId, 0, profileTab.title).setIcon(profileTab.iconResId)
+ menu.add(0, moreTab.navigationRootId, 0, moreTab.title).setIcon(moreTab.iconResId)
+ if (selectedItemId != R.id.profile_nav_graph && selectedItemId != R.id.more_nav_graph && selectedItemId != R.id.favorites_nav_graph) {
+ setBottomNavSelectedTab(profileTab)
+ }
+ return ImmutableList.of(favoriteTab, profileTab, moreTab)
+ }
+
+ private fun setupMainBottomNav(): List {
+ val menu = binding.bottomNavView.menu
+ menu.clear()
+ val navTabList = Utils.getNavTabList(this).first
+ for ((iconResId, title, _, _, _, navigationRootId) in navTabList) {
+ menu.add(0, navigationRootId, 0, title).setIcon(iconResId)
+ }
+ return navTabList
+ }
+
+ private fun setBottomNavSelectedTab(tab: Tab) {
+ binding.bottomNavView.selectedItemId = tab.navigationRootId
+ }
+
+ private fun setBottomNavSelectedTab(@IdRes navGraphRootId: Int) {
+ binding.bottomNavView.selectedItemId = navGraphRootId
+ }
+
+ private fun setupNavigation(toolbar: Toolbar, navController: NavController?) {
+ if (navController == null) return
+ NavigationUI.setupWithNavController(toolbar, navController)
+ navController.addOnDestinationChangedListener(OnDestinationChangedListener { _: NavController?, destination: NavDestination, arguments: Bundle? ->
+ if (destination.id == R.id.directMessagesThreadFragment && arguments != null) {
+ // Set the thread title earlier for better ux
+ val title = arguments.getString("title")
+ val actionBar = supportActionBar
+ if (actionBar != null && !isEmpty(title)) {
+ actionBar.title = title
+ }
+ }
+ // below is a hack to check if we are at the end of the current stack, to setup the search view
+ binding.appBarLayout.setExpanded(true, true)
+ val destinationId = destination.id
+ @SuppressLint("RestrictedApi") val backStack = navController.backStack
+ setupMenu(backStack.size, destinationId)
+ val contains = showBottomViewDestinations.contains(destinationId)
+ binding.root.post {
+ binding.bottomNavView.visibility = if (contains) View.VISIBLE else View.GONE
+ // if (contains) {
+ // behavior?.slideUp(binding.bottomNavView)
+ // }
+ }
+ // explicitly hide keyboard when we navigate
+ val view = currentFocus
+ Utils.hideKeyboard(view)
+ })
+ }
+
+ private fun setupMenu(backStackSize: Int, destinationId: Int) {
+ val searchMenuItem = searchMenuItem ?: return
+ if (backStackSize >= 2 && SEARCH_VISIBLE_DESTINATIONS.contains(destinationId)) {
+ searchMenuItem.isVisible = true
+ return
+ }
+ searchMenuItem.isVisible = false
+ }
+
+ private fun setScrollingBehaviour() {
+ val layoutParams = binding.mainNavHost.layoutParams as CoordinatorLayout.LayoutParams
+ layoutParams.behavior = ScrollingViewBehavior()
+ binding.mainNavHost.requestLayout()
+ }
+
+ private fun removeScrollingBehaviour() {
+ val layoutParams = binding.mainNavHost.layoutParams as CoordinatorLayout.LayoutParams
+ layoutParams.behavior = null
+ binding.mainNavHost.requestLayout()
+ }
+
+ private fun handleIntent(intent: Intent?) {
+ if (intent == null) return
+ val action = intent.action
+ val type = intent.type
+ // Log.d(TAG, action + " " + type);
+ if (Intent.ACTION_MAIN == action) return
+ if (Constants.ACTION_SHOW_ACTIVITY == action) {
+ showActivityView()
+ return
+ }
+ if (Constants.ACTION_SHOW_DM_THREAD == action) {
+ showThread(intent)
+ return
+ }
+ if (Intent.ACTION_SEND == action && type != null) {
+ if (type == "text/plain") {
+ handleUrl(intent.getStringExtra(Intent.EXTRA_TEXT))
+ }
+ return
+ }
+ if (Intent.ACTION_VIEW == action) {
+ val data = intent.data ?: return
+ handleUrl(data.toString())
+ }
+ }
+
+ private fun showThread(intent: Intent) {
+ val threadId = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_ID)
+ val threadTitle = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_TITLE)
+ navigateToThread(threadId, threadTitle)
+ }
+
+ fun navigateToThread(threadId: String?, threadTitle: String?) {
+ if (threadId == null || threadTitle == null) return
+ currentNavControllerLiveData?.observe(this, object : Observer {
+ override fun onChanged(navController: NavController?) {
+ if (navController == null) return
+ if (navController.graph.id != R.id.direct_messages_nav_graph) return
+ try {
+ val currentDestination = navController.currentDestination
+ if (currentDestination != null && currentDestination.id == R.id.directMessagesInboxFragment) {
+ // if we are already on the inbox page, navigate to the thread
+ // need handler.post() to wait for the fragment manager to be ready to navigate
+ Handler(Looper.getMainLooper()).post {
+ val action = DirectMessageInboxFragmentDirections
+ .actionInboxToThread(threadId, threadTitle)
+ navController.navigate(action)
+ }
+ return
+ }
+ // add a destination change listener to navigate to thread once we are on the inbox page
+ navController.addOnDestinationChangedListener(object : OnDestinationChangedListener {
+ override fun onDestinationChanged(
+ controller: NavController,
+ destination: NavDestination,
+ arguments: Bundle?,
+ ) {
+ if (destination.id == R.id.directMessagesInboxFragment) {
+ val action = DirectMessageInboxFragmentDirections
+ .actionInboxToThread(threadId, threadTitle)
+ controller.navigate(action)
+ controller.removeOnDestinationChangedListener(this)
+ }
+ }
+ })
+ // pop back stack until we reach the inbox page
+ navController.popBackStack(R.id.directMessagesInboxFragment, false)
+ } finally {
+ currentNavControllerLiveData?.removeObserver(this)
+ }
+ }
+ })
+ val selectedItemId = binding.bottomNavView.selectedItemId
+ if (selectedItemId != R.navigation.direct_messages_nav_graph) {
+ setBottomNavSelectedTab(R.id.direct_messages_nav_graph)
+ }
+ }
+
+ private fun handleUrl(url: String?) {
+ if (url == null) return
+ // Log.d(TAG, url);
+ val intentModel = IntentUtils.parseUrl(url) ?: return
+ showView(intentModel)
+ }
+
+ private fun showView(intentModel: IntentModel) {
+ when (intentModel.type) {
+ IntentModelType.USERNAME -> showProfileView(intentModel)
+ IntentModelType.POST -> showPostView(intentModel)
+ IntentModelType.LOCATION -> showLocationView(intentModel)
+ IntentModelType.HASHTAG -> showHashtagView(intentModel)
+ IntentModelType.UNKNOWN -> Log.w(TAG, "Unknown model type received!")
+ // else -> Log.w(TAG, "Unknown model type received!")
+ }
+ }
+
+ private fun showProfileView(intentModel: IntentModel) {
+ val username = intentModel.text
+ // Log.d(TAG, "username: " + username);
+ val currentNavControllerLiveData = currentNavControllerLiveData ?: return
+ val navController = currentNavControllerLiveData.value
+ val bundle = Bundle()
+ bundle.putString("username", "@$username")
+ try {
+ navController?.navigate(R.id.action_global_profileFragment, bundle)
+ } catch (e: Exception) {
+ Log.e(TAG, "showProfileView: ", e)
+ }
+ }
+
+ private fun showPostView(intentModel: IntentModel) {
+ val shortCode = intentModel.text
+ // Log.d(TAG, "shortCode: " + shortCode);
+ val alertDialog = AlertDialog.Builder(this)
+ .setCancelable(false)
+ .setView(R.layout.dialog_opening_post)
+ .create()
+ alertDialog.show()
+ lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ val media = if (isLoggedIn) mediaRepository.fetch(shortcodeToId(shortCode)) else graphQLRepository.fetchPost(shortCode)
+ withContext(Dispatchers.Main) {
+ if (media == null) {
+ Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show()
+ return@withContext
+ }
+ val currentNavControllerLiveData = currentNavControllerLiveData ?: return@withContext
+ val navController = currentNavControllerLiveData.value
+ val bundle = Bundle()
+ bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media)
+ try {
+ navController?.navigate(R.id.action_global_post_view, bundle)
+ } catch (e: Exception) {
+ Log.e(TAG, "showPostView: ", e)
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "showPostView: ", e)
+ } finally {
+ withContext(Dispatchers.Main) {
+ alertDialog.dismiss()
+ }
+ }
+ }
+ }
+
+ private fun showLocationView(intentModel: IntentModel) {
+ val locationId = intentModel.text
+ // Log.d(TAG, "locationId: " + locationId);
+ val currentNavControllerLiveData = currentNavControllerLiveData ?: return
+ val navController = currentNavControllerLiveData.value
+ val bundle = Bundle()
+ bundle.putLong("locationId", locationId.toLong())
+ navController?.navigate(R.id.action_global_locationFragment, bundle)
+ }
+
+ private fun showHashtagView(intentModel: IntentModel) {
+ val hashtag = intentModel.text
+ // Log.d(TAG, "hashtag: " + hashtag);
+ val currentNavControllerLiveData = currentNavControllerLiveData ?: return
+ val navController = currentNavControllerLiveData.value
+ val bundle = Bundle()
+ bundle.putString("hashtag", hashtag)
+ navController?.navigate(R.id.action_global_hashTagFragment, bundle)
+ }
+
+ private fun showActivityView() {
+ val currentNavControllerLiveData = currentNavControllerLiveData ?: return
+ val navController = currentNavControllerLiveData.value
+ val bundle = Bundle()
+ bundle.putString("type", "notif")
+ navController?.navigate(R.id.action_global_notificationsViewerFragment, bundle)
+ }
+
+ private fun bindActivityCheckerService() {
+ bindService(Intent(this, ActivityCheckerService::class.java), serviceConnection, BIND_AUTO_CREATE)
+ isActivityCheckerServiceBound = true
+ }
+
+ private fun unbindActivityCheckerService() {
+ if (!isActivityCheckerServiceBound) return
+ unbindService(serviceConnection)
+ isActivityCheckerServiceBound = false
+ }
+
+ val bottomNavView: BottomNavigationView
+ get() = binding.bottomNavView
+
+ fun setCollapsingView(view: View) {
+ try {
+ binding.collapsingToolbarLayout.addView(view, 0)
+ } catch (e: Exception) {
+ Log.e(TAG, "setCollapsingView: ", e)
+ }
+ }
+
+ fun removeCollapsingView(view: View) {
+ try {
+ binding.collapsingToolbarLayout.removeView(view)
+ } catch (e: Exception) {
+ Log.e(TAG, "removeCollapsingView: ", e)
+ }
+ }
+
+ fun resetToolbar() {
+ binding.appBarLayout.visibility = View.VISIBLE
+ setScrollingBehaviour()
+ setSupportActionBar(binding.toolbar)
+ val currentNavControllerLiveData = currentNavControllerLiveData ?: return
+ setupNavigation(binding.toolbar, currentNavControllerLiveData.value)
+ }
+
+ val collapsingToolbarView: CollapsingToolbarLayout
+ get() = binding.collapsingToolbarLayout
+ val appbarLayout: AppBarLayout
+ get() = binding.appBarLayout
+
+ fun removeLayoutTransition() {
+ binding.root.layoutTransition = null
+ }
+
+ fun setLayoutTransition() {
+ binding.root.layoutTransition = LayoutTransition()
+ }
+
+ private fun initEmojiCompat() {
+ // Use a downloadable font for EmojiCompat
+ val fontRequest = FontRequest(
+ "com.google.android.gms.fonts",
+ "com.google.android.gms",
+ "Noto Color Emoji Compat",
+ R.array.com_google_android_gms_fonts_certs)
+ val config: EmojiCompat.Config = FontRequestEmojiCompatConfig(applicationContext, fontRequest)
+ config.setReplaceAll(true) // .setUseEmojiAsDefaultStyle(true)
+ .registerInitCallback(object : InitCallback() {
+ override fun onInitialized() {
+ Log.i(TAG, "EmojiCompat initialized")
+ }
+
+ override fun onFailed(throwable: Throwable?) {
+ Log.e(TAG, "EmojiCompat initialization failed", throwable)
+ }
+ })
+ EmojiCompat.init(config)
+ }
+
+ var toolbar: Toolbar
+ get() = binding.toolbar
+ set(toolbar) {
+ binding.appBarLayout.visibility = View.GONE
+ removeScrollingBehaviour()
+ setSupportActionBar(toolbar)
+ if (currentNavControllerLiveData == null) return
+ setupNavigation(toolbar, currentNavControllerLiveData?.value)
+ }
+ val rootView: View
+ get() = binding.root
+
+ private fun setNavBarDMUnreadCountBadge(unseenCount: Int) {
+ val badge = binding.bottomNavView.getOrCreateBadge(R.id.direct_messages_nav_graph)
+ if (unseenCount == 0) {
+ badge.isVisible = false
+ badge.clearNumber()
+ return
+ }
+ if (badge.verticalOffset != 10) {
+ badge.verticalOffset = 10
+ }
+ badge.number = unseenCount
+ badge.isVisible = true
+ }
+
+ fun showSearchView(): TextInputLayout {
+ binding.searchInputLayout.visibility = View.VISIBLE
+ return binding.searchInputLayout
+ }
+
+ fun hideSearchView() {
+ binding.searchInputLayout.visibility = View.GONE
+ }
+
+ companion object {
+ private const val TAG = "MainActivity"
+ private const val FIRST_FRAGMENT_GRAPH_INDEX_KEY = "firstFragmentGraphIndex"
+ private const val LAST_SELECT_NAV_MENU_ID = "lastSelectedNavMenuId"
+ private val SEARCH_VISIBLE_DESTINATIONS: List = ImmutableList.of(
+ R.id.feedFragment,
+ R.id.profileFragment,
+ R.id.directMessagesInboxFragment,
+ R.id.discoverFragment,
+ R.id.favoritesFragment,
+ R.id.hashTagFragment,
+ R.id.locationFragment
+ )
+
+ @JvmStatic
+ var instance: MainActivity? = null
+ private set
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java
index d88e5f34..58f7d1fa 100755
--- a/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java
+++ b/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java
@@ -1,195 +1,60 @@
package awais.instagrabber.adapters;
-import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
-import androidx.recyclerview.widget.RecyclerView;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.Objects;
-import awais.instagrabber.adapters.viewholder.comments.ChildCommentViewHolder;
-import awais.instagrabber.adapters.viewholder.comments.ParentCommentViewHolder;
+import awais.instagrabber.adapters.viewholder.CommentViewHolder;
import awais.instagrabber.databinding.ItemCommentBinding;
-import awais.instagrabber.databinding.ItemCommentSmallBinding;
-import awais.instagrabber.models.CommentModel;
+import awais.instagrabber.models.Comment;
-public final class CommentsAdapter extends ListAdapter {
- private static final int TYPE_PARENT = 1;
- private static final int TYPE_CHILD = 2;
-
- private final Map positionTypeMap = new HashMap<>();
-
- // private final Filter filter = new Filter() {
- // @NonNull
- // @Override
- // protected FilterResults performFiltering(final CharSequence filter) {
- // final FilterResults results = new FilterResults();
- // results.values = commentModels;
- //
- // final int commentsLen = commentModels == null ? 0 : commentModels.size();
- // if (commentModels != null && commentsLen > 0 && !TextUtils.isEmpty(filter)) {
- // final String query = filter.toString().toLowerCase();
- // final ArrayList filterList = new ArrayList<>(commentsLen);
- //
- // for (final CommentModel commentModel : commentModels) {
- // final String commentText = commentModel.getText().toString().toLowerCase();
- //
- // if (commentText.contains(query)) filterList.add(commentModel);
- // else {
- // final List childCommentModels = commentModel.getChildCommentModels();
- // if (childCommentModels != null) {
- // for (final CommentModel childCommentModel : childCommentModels) {
- // final String childCommentText = childCommentModel.getText().toString().toLowerCase();
- // if (childCommentText.contains(query)) filterList.add(commentModel);
- // }
- // }
- // }
- // }
- // filterList.trimToSize();
- // results.values = filterList.toArray(new CommentModel[0]);
- // }
- //
- // return results;
- // }
- //
- // @Override
- // protected void publishResults(final CharSequence constraint, @NonNull final FilterResults results) {
- // if (results.values instanceof List) {
- // //noinspection unchecked
- // filteredCommentModels = (List) results.values;
- // notifyDataSetChanged();
- // }
- // }
- // };
-
- private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
+public final class CommentsAdapter extends ListAdapter {
+ private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
- public boolean areItemsTheSame(@NonNull final CommentModel oldItem, @NonNull final CommentModel newItem) {
- return oldItem.getId().equals(newItem.getId());
+ public boolean areItemsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) {
+ return Objects.equals(oldItem.getPk(), newItem.getPk());
}
@Override
- public boolean areContentsTheSame(@NonNull final CommentModel oldItem, @NonNull final CommentModel newItem) {
- return oldItem.getId().equals(newItem.getId());
+ public boolean areContentsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) {
+ return Objects.equals(oldItem, newItem);
}
};
- private final CommentCallback commentCallback;
- private CommentModel selected, toChangeLike;
- private int selectedIndex, likedIndex;
- public CommentsAdapter(final CommentCallback commentCallback) {
+ private final boolean showingReplies;
+ private final CommentCallback commentCallback;
+ private final long currentUserId;
+
+ public CommentsAdapter(final long currentUserId,
+ final boolean showingReplies,
+ final CommentCallback commentCallback) {
super(DIFF_CALLBACK);
+ this.showingReplies = showingReplies;
+ this.currentUserId = currentUserId;
this.commentCallback = commentCallback;
}
@NonNull
@Override
- public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
- final Context context = parent.getContext();
- final LayoutInflater layoutInflater = LayoutInflater.from(context);
- if (type == TYPE_PARENT) {
- final ItemCommentBinding binding = ItemCommentBinding.inflate(layoutInflater, parent, false);
- return new ParentCommentViewHolder(binding);
- }
- final ItemCommentSmallBinding binding = ItemCommentSmallBinding.inflate(layoutInflater, parent, false);
- return new ChildCommentViewHolder(binding);
+ public CommentViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
+ final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
+ final ItemCommentBinding binding = ItemCommentBinding.inflate(layoutInflater, parent, false);
+ return new CommentViewHolder(binding, currentUserId, commentCallback);
}
@Override
- public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
- CommentModel commentModel = getItem(position);
- if (commentModel == null) return;
- final int type = getItemViewType(position);
- final boolean selected = this.selected != null && this.selected.getId().equals(commentModel.getId());
- final boolean toLike = this.toChangeLike != null && this.toChangeLike.getId().equals(commentModel.getId());
- if (toLike) commentModel = this.toChangeLike;
- if (type == TYPE_PARENT) {
- final ParentCommentViewHolder viewHolder = (ParentCommentViewHolder) holder;
- viewHolder.bind(commentModel, selected, commentCallback);
- return;
- }
- final ChildCommentViewHolder viewHolder = (ChildCommentViewHolder) holder;
- viewHolder.bind(commentModel, selected, commentCallback);
- }
-
- @Override
- public void submitList(@Nullable final List list) {
- final List flatList = flattenList(list);
- super.submitList(flatList);
- }
-
- @Override
- public void submitList(@Nullable final List list, @Nullable final Runnable commitCallback) {
- final List flatList = flattenList(list);
- super.submitList(flatList, commitCallback);
- }
-
- private List flattenList(final List list) {
- if (list == null) {
- return Collections.emptyList();
- }
- final List flatList = new ArrayList<>();
- int lastCommentIndex = -1;
- for (final CommentModel parent : list) {
- lastCommentIndex++;
- flatList.add(parent);
- positionTypeMap.put(lastCommentIndex, TYPE_PARENT);
- final List children = parent.getChildCommentModels();
- if (children != null) {
- for (final CommentModel child : children) {
- lastCommentIndex++;
- flatList.add(child);
- positionTypeMap.put(lastCommentIndex, TYPE_CHILD);
- }
- }
- }
- return flatList;
- }
-
-
- @Override
- public int getItemViewType(final int position) {
- final Integer type = positionTypeMap.get(position);
- if (type == null) {
- return TYPE_PARENT;
- }
- return type;
- }
-
- public void setSelected(final CommentModel commentModel) {
- this.selected = commentModel;
- selectedIndex = getCurrentList().indexOf(commentModel);
- notifyItemChanged(selectedIndex);
- }
-
- public void clearSelection() {
- this.selected = null;
- notifyItemChanged(selectedIndex);
- }
-
- public void setLiked(final CommentModel commentModel, final boolean liked) {
- likedIndex = getCurrentList().indexOf(commentModel);
- CommentModel newCommentModel = commentModel;
- newCommentModel.setLiked(liked);
- this.toChangeLike = newCommentModel;
- notifyItemChanged(likedIndex);
- }
-
- public CommentModel getSelected() {
- return selected;
+ public void onBindViewHolder(@NonNull final CommentViewHolder holder, final int position) {
+ final Comment comment = getItem(position);
+ holder.bind(comment, showingReplies && position == 0, showingReplies && position != 0);
}
public interface CommentCallback {
- void onClick(final CommentModel comment);
+ void onClick(final Comment comment);
void onHashtagClick(final String hashtag);
@@ -198,5 +63,15 @@ public final class CommentsAdapter extends ListAdapter {
private static final String TAG = DirectItemsAdapter.class.getSimpleName();
@@ -139,7 +139,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter sectionAndSort(final List list) {
final List itemOrHeaders = new ArrayList<>();
- Date prevSectionDate = null;
+ LocalDate prevSectionDate = null;
for (int i = 0; i < list.size(); i++) {
final DirectItem item = list.get(i);
- if (item == null) continue;
+ if (item == null || item.getDate() == null) continue;
final DirectItemOrHeader prev = itemOrHeaders.isEmpty() ? null : itemOrHeaders.get(itemOrHeaders.size() - 1);
- if (prev != null && prev.item != null && DateUtils.isSameDay(prev.item.getDate(), item.getDate())) {
+ if (prev != null
+ && prev.item != null
+ && prev.item.getDate() != null
+ && prev.item.getDate().toLocalDate().isEqual(item.getDate().toLocalDate())) {
// just add item
final DirectItemOrHeader itemOrHeader = new DirectItemOrHeader();
itemOrHeader.item = item;
@@ -320,7 +323,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter callback);
+
+ void onAddReactionListener(DirectItem item);
}
public interface DirectItemInternalLongClickListener {
diff --git a/app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java
index 4623e230..a1547a0b 100644
--- a/app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java
+++ b/app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.discover.TopicCluster;
+import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.ResponseBodyUtils;
public class DiscoverTopicsAdapter extends ListAdapter {
@@ -50,6 +51,8 @@ public class DiscoverTopicsAdapter extends ListAdapter {
private final OnFeedStoryClickListener listener;
@@ -29,7 +22,7 @@ public final class FeedStoriesAdapter extends ListAdapter list;
private final Filter filter = new Filter() {
- @Nullable
+ @NonNull
@Override
protected FilterResults performFiltering(final CharSequence filter) {
- final boolean isFilterEmpty = TextUtils.isEmpty(filter);
- final String query = isFilterEmpty ? null : filter.toString().toLowerCase();
-
- for (FeedStoryModel item : list) {
- if (isFilterEmpty) item.setShown(true);
- else item.setShown(item.getProfileModel().getUsername().toLowerCase().contains(query));
+ final String query = TextUtils.isEmpty(filter) ? null : filter.toString().toLowerCase();
+ List filteredList = list;
+ if (list != null && query != null) {
+ filteredList = list.stream()
+ .filter(feedStoryModel -> feedStoryModel.getProfileModel()
+ .getUsername()
+ .toLowerCase()
+ .contains(query))
+ .collect(Collectors.toList());
}
- return null;
+ final FilterResults filterResults = new FilterResults();
+ filterResults.count = filteredList != null ? filteredList.size() : 0;
+ filterResults.values = filteredList;
+ return filterResults;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
- submitList(list);
- notifyDataSetChanged();
+ //noinspection unchecked
+ submitList((List) results.values, true);
}
};
@@ -51,7 +57,7 @@ public final class FeedStoriesListAdapter extends ListAdapter list, final boolean isFiltered) {
+ if (!isFiltered) {
+ this.list = list;
+ }
+ super.submitList(list);
+ }
+
@Override
public void submitList(final List list) {
- super.submitList(list.stream().filter(i -> i.isShown()).collect(Collectors.toList()));
- this.list = list;
+ submitList(list, false);
}
@NonNull
@@ -82,11 +94,11 @@ public final class FeedStoriesListAdapter extends ListAdapter
@Override
public void onBindViewHolder(@NonNull final FollowsViewHolder holder, final int position) {
final User model = profileModels.get(position);
- holder.bind(model, null, onClickListener);
+ holder.bind(model, onClickListener);
}
@Override
diff --git a/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java
index 80ab465b..9a9748e2 100644
--- a/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java
+++ b/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java
@@ -23,8 +23,8 @@ public final class NotificationsAdapter extends ListAdapter DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
- public boolean areItemsTheSame(@NonNull final Notification oldItem, @NonNull final Notification newItem) {
- return oldItem.getPk().equals(newItem.getPk());
+ public boolean areItemsTheSame(final Notification oldItem, final Notification newItem) {
+ return oldItem != null && newItem != null && oldItem.getPk().equals(newItem.getPk());
}
@Override
diff --git a/app/src/main/java/awais/instagrabber/adapters/SavedCollectionsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SavedCollectionsAdapter.java
index 8d9ff290..0d6e361a 100644
--- a/app/src/main/java/awais/instagrabber/adapters/SavedCollectionsAdapter.java
+++ b/app/src/main/java/awais/instagrabber/adapters/SavedCollectionsAdapter.java
@@ -11,20 +11,19 @@ import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
-import awais.instagrabber.utils.ResponseBodyUtils;
public class SavedCollectionsAdapter extends ListAdapter {
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final SavedCollection oldItem, @NonNull final SavedCollection newItem) {
- return oldItem.getId().equals(newItem.getId());
+ return oldItem.getCollectionId().equals(newItem.getCollectionId());
}
@Override
public boolean areContentsTheSame(@NonNull final SavedCollection oldItem, @NonNull final SavedCollection newItem) {
- if (oldItem.getCoverMedias() != null && newItem.getCoverMedias() != null
- && oldItem.getCoverMedias().size() == newItem.getCoverMedias().size()) {
- return oldItem.getCoverMedias().get(0).getId().equals(newItem.getCoverMedias().get(0).getId());
+ if (oldItem.getCoverMediaList() != null && newItem.getCoverMediaList() != null
+ && oldItem.getCoverMediaList().size() == newItem.getCoverMediaList().size()) {
+ return oldItem.getCoverMediaList().get(0).getId().equals(newItem.getCoverMediaList().get(0).getId());
}
else if (oldItem.getCoverMedia() != null && newItem.getCoverMedia() != null) {
return oldItem.getCoverMedia().getId().equals(newItem.getCoverMedia().getId());
diff --git a/app/src/main/java/awais/instagrabber/adapters/SearchCategoryAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SearchCategoryAdapter.java
new file mode 100644
index 00000000..9f6887fd
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/adapters/SearchCategoryAdapter.java
@@ -0,0 +1,33 @@
+package awais.instagrabber.adapters;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+
+import java.util.List;
+
+import awais.instagrabber.fragments.search.SearchCategoryFragment;
+import awais.instagrabber.models.enums.FavoriteType;
+
+public class SearchCategoryAdapter extends FragmentStateAdapter {
+
+ private final List categories;
+
+ public SearchCategoryAdapter(@NonNull final Fragment fragment,
+ @NonNull final List categories) {
+ super(fragment);
+ this.categories = categories;
+
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(final int position) {
+ return SearchCategoryFragment.newInstance(categories.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return categories.size();
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/adapters/SearchItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SearchItemsAdapter.java
new file mode 100644
index 00000000..e26059de
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/adapters/SearchItemsAdapter.java
@@ -0,0 +1,215 @@
+package awais.instagrabber.adapters;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.AdapterListUpdateCallback;
+import androidx.recyclerview.widget.AsyncDifferConfig;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import awais.instagrabber.R;
+import awais.instagrabber.adapters.viewholder.SearchItemViewHolder;
+import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
+import awais.instagrabber.databinding.ItemSearchResultBinding;
+import awais.instagrabber.fragments.search.SearchCategoryFragment.OnSearchItemClickListener;
+import awais.instagrabber.models.enums.FavoriteType;
+import awais.instagrabber.repositories.responses.search.SearchItem;
+
+public final class SearchItemsAdapter extends RecyclerView.Adapter {
+ private static final String TAG = SearchItemsAdapter.class.getSimpleName();
+ private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
+ @Override
+ public boolean areItemsTheSame(@NonNull final SearchItemOrHeader oldItem, @NonNull final SearchItemOrHeader newItem) {
+ return Objects.equals(oldItem, newItem);
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull final SearchItemOrHeader oldItem, @NonNull final SearchItemOrHeader newItem) {
+ return Objects.equals(oldItem, newItem);
+ }
+ };
+ private static final String RECENT = "recent";
+ private static final String FAVORITE = "favorite";
+ private static final int VIEW_TYPE_HEADER = 0;
+ private static final int VIEW_TYPE_ITEM = 1;
+
+ private final OnSearchItemClickListener onSearchItemClickListener;
+ private final AsyncListDiffer differ;
+
+ public SearchItemsAdapter(final OnSearchItemClickListener onSearchItemClickListener) {
+ differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
+ new AsyncDifferConfig.Builder<>(DIFF_CALLBACK).build());
+ this.onSearchItemClickListener = onSearchItemClickListener;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
+ final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
+ if (viewType == VIEW_TYPE_HEADER) {
+ return new HeaderViewHolder(ItemFavSectionHeaderBinding.inflate(layoutInflater, parent, false));
+ }
+ final ItemSearchResultBinding binding = ItemSearchResultBinding.inflate(layoutInflater, parent, false);
+ return new SearchItemViewHolder(binding, onSearchItemClickListener);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
+ if (getItemViewType(position) == VIEW_TYPE_HEADER) {
+ final SearchItemOrHeader searchItemOrHeader = getItem(position);
+ if (!searchItemOrHeader.isHeader()) return;
+ ((HeaderViewHolder) holder).bind(searchItemOrHeader.header);
+ return;
+ }
+ ((SearchItemViewHolder) holder).bind(getItem(position).searchItem);
+ }
+
+ protected SearchItemOrHeader getItem(int position) {
+ return differ.getCurrentList().get(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return differ.getCurrentList().size();
+ }
+
+ @Override
+ public int getItemViewType(final int position) {
+ return getItem(position).isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM;
+ }
+
+ public void submitList(@Nullable final List list) {
+ if (list == null) {
+ differ.submitList(null);
+ return;
+ }
+ differ.submitList(sectionAndSort(list));
+ }
+
+ public void submitList(@Nullable final List list, @Nullable final Runnable commitCallback) {
+ if (list == null) {
+ differ.submitList(null, commitCallback);
+ return;
+ }
+ differ.submitList(sectionAndSort(list), commitCallback);
+ }
+
+ @NonNull
+ private List sectionAndSort(@NonNull final List list) {
+ final boolean containsRecentOrFavorite = list.stream().anyMatch(searchItem -> searchItem.isRecent() || searchItem.isFavorite());
+ // Don't do anything if not showing recent results
+ if (!containsRecentOrFavorite) {
+ return list.stream()
+ .map(SearchItemOrHeader::new)
+ .collect(Collectors.toList());
+ }
+ final List listCopy = new ArrayList<>(list);
+ Collections.sort(listCopy, (o1, o2) -> {
+ final boolean bothRecent = o1.isRecent() && o2.isRecent();
+ if (bothRecent) {
+ // Don't sort
+ return 0;
+ }
+ final boolean bothFavorite = o1.isFavorite() && o2.isFavorite();
+ if (bothFavorite) {
+ if (o1.getType() == o2.getType()) return 0;
+ // keep users at top
+ if (o1.getType() == FavoriteType.USER) return -1;
+ if (o2.getType() == FavoriteType.USER) return 1;
+ // keep locations at bottom
+ if (o1.getType() == FavoriteType.LOCATION) return 1;
+ if (o2.getType() == FavoriteType.LOCATION) return -1;
+ }
+ // keep recents at top
+ if (o1.isRecent()) return -1;
+ if (o2.isRecent()) return 1;
+ return 0;
+ });
+ final List itemOrHeaders = new ArrayList<>();
+ for (int i = 0; i < listCopy.size(); i++) {
+ final SearchItem searchItem = listCopy.get(i);
+ final SearchItemOrHeader prev = itemOrHeaders.isEmpty() ? null : itemOrHeaders.get(itemOrHeaders.size() - 1);
+ boolean prevWasSameType = prev != null && ((prev.searchItem.isRecent() && searchItem.isRecent())
+ || (prev.searchItem.isFavorite() && searchItem.isFavorite()));
+ if (prevWasSameType) {
+ // just add the item
+ itemOrHeaders.add(new SearchItemOrHeader(searchItem));
+ continue;
+ }
+ // add header and item
+ // add header only if search item is recent or favorite
+ if (searchItem.isRecent() || searchItem.isFavorite()) {
+ itemOrHeaders.add(new SearchItemOrHeader(searchItem.isRecent() ? RECENT : FAVORITE));
+ }
+ itemOrHeaders.add(new SearchItemOrHeader(searchItem));
+ }
+ return itemOrHeaders;
+ }
+
+ private static class SearchItemOrHeader {
+ String header;
+ SearchItem searchItem;
+
+ public SearchItemOrHeader(final SearchItem searchItem) {
+ this.searchItem = searchItem;
+ }
+
+ public SearchItemOrHeader(final String header) {
+ this.header = header;
+ }
+
+ boolean isHeader() {
+ return header != null;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final SearchItemOrHeader that = (SearchItemOrHeader) o;
+ return Objects.equals(header, that.header) &&
+ Objects.equals(searchItem, that.searchItem);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(header, searchItem);
+ }
+ }
+
+ public static class HeaderViewHolder extends RecyclerView.ViewHolder {
+ private final ItemFavSectionHeaderBinding binding;
+
+ public HeaderViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(final String header) {
+ if (header == null) return;
+ final int headerText;
+ switch (header) {
+ case RECENT:
+ headerText = R.string.recent;
+ break;
+ case FAVORITE:
+ headerText = R.string.title_favorites;
+ break;
+ default:
+ headerText = R.string.unknown;
+ break;
+ }
+ binding.getRoot().setText(headerText);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/adapters/SliderCallbackAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SliderCallbackAdapter.java
index 1cfec468..a92192eb 100644
--- a/app/src/main/java/awais/instagrabber/adapters/SliderCallbackAdapter.java
+++ b/app/src/main/java/awais/instagrabber/adapters/SliderCallbackAdapter.java
@@ -1,11 +1,17 @@
package awais.instagrabber.adapters;
+import android.view.View;
+
+import com.google.android.exoplayer2.ui.StyledPlayerView;
+
+import awais.instagrabber.repositories.responses.Media;
+
public class SliderCallbackAdapter implements SliderItemsAdapter.SliderCallback {
@Override
public void onThumbnailLoaded(final int position) {}
@Override
- public void onItemClicked(final int position) {}
+ public void onItemClicked(final int position, final Media media, final View view) {}
@Override
public void onPlayerPlay(final int position) {}
@@ -15,4 +21,12 @@ public class SliderCallbackAdapter implements SliderItemsAdapter.SliderCallback
@Override
public void onPlayerRelease(final int position) {}
+
+ @Override
+ public void onFullScreenModeChanged(final boolean isFullScreen, final StyledPlayerView playerView) {}
+
+ @Override
+ public boolean isInFullScreen() {
+ return false;
+ }
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java
index c805d316..2d036fb1 100644
--- a/app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java
+++ b/app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java
@@ -1,28 +1,27 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
+import com.google.android.exoplayer2.ui.StyledPlayerView;
+
import awais.instagrabber.adapters.viewholder.SliderItemViewHolder;
import awais.instagrabber.adapters.viewholder.SliderPhotoViewHolder;
import awais.instagrabber.adapters.viewholder.SliderVideoViewHolder;
-import awais.instagrabber.customviews.VerticalDragHelper;
import awais.instagrabber.databinding.ItemSliderPhotoBinding;
-import awais.instagrabber.databinding.LayoutExoCustomControlsBinding;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
public final class SliderItemsAdapter extends ListAdapter {
- private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener;
private final boolean loadVideoOnItemClick;
private final SliderCallback sliderCallback;
- private final LayoutExoCustomControlsBinding controlsBinding;
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
@@ -36,15 +35,11 @@ public final class SliderItemsAdapter extends ListAdapter onSuggestionClickListener.onSuggestionClick(finalSuggestionType, query));
- }
- final ItemSuggestionBinding binding = ItemSuggestionBinding.bind(view);
- binding.isVerified.setVisibility(verified ? View.VISIBLE : View.GONE);
- binding.tvUsername.setText(username);
- binding.tvFullName.setVisibility(View.VISIBLE);
- binding.tvFullName.setText(fullName);
- binding.ivProfilePic.setImageURI(picUrl);
- }
-
- public interface OnSuggestionClickListener {
- void onSuggestionClick(final SuggestionType type, final String query);
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java
new file mode 100644
index 00000000..0643f7cd
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java
@@ -0,0 +1,209 @@
+package awais.instagrabber.adapters.viewholder;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.View;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.appcompat.view.ContextThemeWrapper;
+import androidx.appcompat.widget.PopupMenu;
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.RecyclerView;
+
+import awais.instagrabber.R;
+import awais.instagrabber.adapters.CommentsAdapter.CommentCallback;
+import awais.instagrabber.customviews.ProfilePicView;
+import awais.instagrabber.databinding.ItemCommentBinding;
+import awais.instagrabber.models.Comment;
+import awais.instagrabber.repositories.responses.User;
+import awais.instagrabber.utils.Utils;
+
+public final class CommentViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemCommentBinding binding;
+ private final long currentUserId;
+ private final CommentCallback commentCallback;
+ @ColorInt
+ private int parentCommentHighlightColor;
+ private PopupMenu optionsPopup;
+
+ public CommentViewHolder(@NonNull final ItemCommentBinding binding,
+ final long currentUserId,
+ final CommentCallback commentCallback) {
+ super(binding.getRoot());
+ this.binding = binding;
+ this.currentUserId = currentUserId;
+ this.commentCallback = commentCallback;
+ final Context context = itemView.getContext();
+ if (context == null) return;
+ final Resources.Theme theme = context.getTheme();
+ if (theme == null) return;
+ final TypedValue typedValue = new TypedValue();
+ final boolean resolved = theme.resolveAttribute(R.attr.parentCommentHighlightColor, typedValue, true);
+ if (resolved) {
+ parentCommentHighlightColor = typedValue.data;
+ }
+ }
+
+ public void bind(final Comment comment, final boolean isReplyParent, final boolean isReply) {
+ if (comment == null) return;
+ itemView.setOnClickListener(v -> {
+ if (commentCallback != null) {
+ commentCallback.onClick(comment);
+ }
+ });
+ if (isReplyParent && parentCommentHighlightColor != 0) {
+ itemView.setBackgroundColor(parentCommentHighlightColor);
+ } else {
+ itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent));
+ }
+ setupCommentText(comment, isReply);
+ binding.date.setText(comment.getDateTime());
+ setLikes(comment, isReply);
+ setReplies(comment, isReply);
+ setUser(comment, isReply);
+ setupOptions(comment, isReply);
+ }
+
+ private void setupCommentText(@NonNull final Comment comment, final boolean isReply) {
+ binding.comment.clearOnURLClickListeners();
+ binding.comment.clearOnHashtagClickListeners();
+ binding.comment.clearOnMentionClickListeners();
+ binding.comment.clearOnEmailClickListeners();
+ binding.comment.setText(comment.getText());
+ binding.comment.setTextSize(TypedValue.COMPLEX_UNIT_SP, isReply ? 12 : 14);
+ binding.comment.addOnHashtagListener(autoLinkItem -> {
+ final String originalText = autoLinkItem.getOriginalText();
+ if (commentCallback == null) return;
+ commentCallback.onHashtagClick(originalText);
+ });
+ binding.comment.addOnMentionClickListener(autoLinkItem -> {
+ final String originalText = autoLinkItem.getOriginalText();
+ if (commentCallback == null) return;
+ commentCallback.onMentionClick(originalText);
+
+ });
+ binding.comment.addOnEmailClickListener(autoLinkItem -> {
+ final String originalText = autoLinkItem.getOriginalText();
+ if (commentCallback == null) return;
+ commentCallback.onEmailClick(originalText);
+ });
+ binding.comment.addOnURLClickListener(autoLinkItem -> {
+ final String originalText = autoLinkItem.getOriginalText();
+ if (commentCallback == null) return;
+ commentCallback.onURLClick(originalText);
+ });
+ binding.comment.setOnLongClickListener(v -> {
+ Utils.copyText(itemView.getContext(), comment.getText());
+ return true;
+ });
+ binding.comment.setOnClickListener(v -> commentCallback.onClick(comment));
+ }
+
+ private void setUser(@NonNull final Comment comment, final boolean isReply) {
+ final User user = comment.getUser();
+ if (user == null) return;
+ binding.username.setUsername(user.getUsername(), user.isVerified());
+ binding.username.setTextAppearance(itemView.getContext(), isReply ? R.style.TextAppearance_MaterialComponents_Subtitle2
+ : R.style.TextAppearance_MaterialComponents_Subtitle1);
+ binding.username.setOnClickListener(v -> {
+ if (commentCallback == null) return;
+ commentCallback.onMentionClick("@" + user.getUsername());
+ });
+ binding.profilePic.setImageURI(user.getProfilePicUrl());
+ binding.profilePic.setSize(isReply ? ProfilePicView.Size.SMALLER : ProfilePicView.Size.SMALL);
+ binding.profilePic.setOnClickListener(v -> {
+ if (commentCallback == null) return;
+ commentCallback.onMentionClick("@" + user.getUsername());
+ });
+ }
+
+ private void setLikes(@NonNull final Comment comment, final boolean isReply) {
+ // final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes);
+ binding.likes.setText(String.valueOf(comment.getCommentLikeCount()));
+ binding.likes.setOnLongClickListener(v -> {
+ if (commentCallback == null) return false;
+ commentCallback.onViewLikes(comment);
+ return true;
+ });
+ if (currentUserId == 0) { // not logged in
+ binding.likes.setOnClickListener(v -> {
+ if (commentCallback == null) return;
+ commentCallback.onViewLikes(comment);
+ });
+ return;
+ }
+ final boolean liked = comment.getLiked();
+ final int resId = liked ? R.drawable.ic_like : R.drawable.ic_not_liked;
+ binding.likes.setCompoundDrawablesRelativeWithSize(ContextCompat.getDrawable(itemView.getContext(), resId), null, null, null);
+ binding.likes.setOnClickListener(v -> {
+ if (commentCallback == null) return;
+ // toggle like
+ commentCallback.onLikeClick(comment, !liked, isReply);
+ });
+ }
+
+ private void setReplies(@NonNull final Comment comment, final boolean isReply) {
+ final int replies = comment.getChildCommentCount();
+ binding.replies.setVisibility(View.VISIBLE);
+ final String text = isReply ? "" : String.valueOf(replies);
+ // final String string = itemView.getResources().getQuantityString(R.plurals.replies_count, replies, replies);
+ binding.replies.setText(text);
+ binding.replies.setOnClickListener(v -> {
+ if (commentCallback == null) return;
+ commentCallback.onRepliesClick(comment);
+ });
+ }
+
+ private void setupOptions(final Comment comment, final boolean isReply) {
+ binding.options.setOnClickListener(v -> {
+ if (optionsPopup == null) {
+ createOptionsPopupMenu(comment, isReply);
+ }
+ if (optionsPopup == null) return;
+ optionsPopup.show();
+ });
+ }
+
+ private void createOptionsPopupMenu(final Comment comment, final boolean isReply) {
+ if (optionsPopup == null) {
+ final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(itemView.getContext(), R.style.popupMenuStyle);
+ optionsPopup = new PopupMenu(themeWrapper, binding.options);
+ } else {
+ optionsPopup.getMenu().clear();
+ }
+ optionsPopup.getMenuInflater().inflate(R.menu.comment_options_menu, optionsPopup.getMenu());
+ final User user = comment.getUser();
+ if (currentUserId == 0 || user == null || user.getPk() != currentUserId) {
+ final Menu menu = optionsPopup.getMenu();
+ menu.removeItem(R.id.delete);
+ }
+ optionsPopup.setOnMenuItemClickListener(item -> {
+ if (commentCallback == null) return false;
+ int itemId = item.getItemId();
+ if (itemId == R.id.translate) {
+ commentCallback.onTranslate(comment);
+ return true;
+ }
+ if (itemId == R.id.delete) {
+ commentCallback.onDelete(comment, isReply);
+ }
+ return true;
+ });
+ }
+
+ // private void setupReply(final Comment comment) {
+ // if (!isLoggedIn) {
+ // binding.reply.setVisibility(View.GONE);
+ // return;
+ // }
+ // binding.reply.setOnClickListener(v -> {
+ // if (commentCallback == null) return;
+ // // toggle like
+ // commentCallback.onReplyClick(comment);
+ // });
+ // }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/DiscoverViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/DiscoverViewHolder.java
index 1dbd28a0..94735486 100755
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/DiscoverViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/DiscoverViewHolder.java
@@ -1,26 +1,26 @@
-package awais.instagrabber.adapters.viewholder;
-
-import android.view.View;
-import android.widget.ImageView;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.facebook.drawee.view.SimpleDraweeView;
-
-import awais.instagrabber.R;
-
-public final class DiscoverViewHolder extends RecyclerView.ViewHolder {
- public final SimpleDraweeView postImage;
- public final ImageView typeIcon;
- public final View selectedView;
- // public final View progressView;
-
- public DiscoverViewHolder(@NonNull final View itemView) {
- super(itemView);
- typeIcon = itemView.findViewById(R.id.typeIcon);
- postImage = itemView.findViewById(R.id.postImage);
- selectedView = itemView.findViewById(R.id.selectedView);
- // progressView = itemView.findViewById(R.id.progressView);
- }
-}
\ No newline at end of file
+//package awais.instagrabber.adapters.viewholder;
+//
+//import android.view.View;
+//import android.widget.ImageView;
+//
+//import androidx.annotation.NonNull;
+//import androidx.recyclerview.widget.RecyclerView;
+//
+//import com.facebook.drawee.view.SimpleDraweeView;
+//
+//import awais.instagrabber.R;
+//
+//public final class DiscoverViewHolder extends RecyclerView.ViewHolder {
+// public final SimpleDraweeView postImage;
+// public final ImageView typeIcon;
+// public final View selectedView;
+// // public final View progressView;
+//
+// public DiscoverViewHolder(@NonNull final View itemView) {
+// super(itemView);
+// typeIcon = itemView.findViewById(R.id.typeIcon);
+// postImage = itemView.findViewById(R.id.postImage);
+// selectedView = itemView.findViewById(R.id.selectedView);
+// // progressView = itemView.findViewById(R.id.progressView);
+// }
+//}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java
index e16824ea..bf9da891 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java
@@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.adapters.FavoritesAdapter;
-import awais.instagrabber.databinding.ItemSuggestionBinding;
+import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.utils.Constants;
@@ -14,12 +14,12 @@ import awais.instagrabber.utils.Constants;
public class FavoriteViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = "FavoriteViewHolder";
- private final ItemSuggestionBinding binding;
+ private final ItemSearchResultBinding binding;
- public FavoriteViewHolder(@NonNull final ItemSuggestionBinding binding) {
+ public FavoriteViewHolder(@NonNull final ItemSearchResultBinding binding) {
super(binding.getRoot());
this.binding = binding;
- binding.isVerified.setVisibility(View.GONE);
+ binding.verified.setVisibility(View.GONE);
}
public void bind(final Favorite model,
@@ -36,12 +36,12 @@ public class FavoriteViewHolder extends RecyclerView.ViewHolder {
return longClickListener.onLongClick(model);
});
if (model.getType() == FavoriteType.HASHTAG) {
- binding.ivProfilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC);
+ binding.profilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC);
} else {
- binding.ivProfilePic.setImageURI(model.getPicUrl());
+ binding.profilePic.setImageURI(model.getPicUrl());
}
- binding.tvFullName.setText(model.getDisplayName());
- binding.tvUsername.setVisibility(View.VISIBLE);
+ binding.title.setVisibility(View.VISIBLE);
+ binding.subtitle.setText(model.getDisplayName());
String query = model.getQuery();
switch (model.getType()) {
case HASHTAG:
@@ -51,11 +51,11 @@ public class FavoriteViewHolder extends RecyclerView.ViewHolder {
query = "@" + query;
break;
case LOCATION:
- binding.tvUsername.setVisibility(View.GONE);
+ binding.title.setVisibility(View.GONE);
break;
default:
// do nothing
}
- binding.tvUsername.setText(query);
+ binding.title.setText(query);
}
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java
index c6e1d571..af869af1 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java
@@ -19,11 +19,12 @@ import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FeedAdapterV2;
-import awais.instagrabber.asyncs.DownloadedCheckerAsyncTask;
import awais.instagrabber.databinding.ItemFeedGridBinding;
import awais.instagrabber.models.PostsLayoutPreferences;
+import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
+import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
@@ -68,7 +69,9 @@ public class FeedGridItemViewHolder extends RecyclerView.ViewHolder {
setUserDetails(media, layoutPreferences);
String thumbnailUrl = null;
final int typeIconRes;
- switch (media.getMediaType()) {
+ final MediaItemType mediaType = media.getMediaType();
+ if (mediaType == null) return;
+ switch (mediaType) {
case MEDIA_TYPE_IMAGE:
typeIconRes = -1;
thumbnailUrl = ResponseBodyUtils.getThumbUrl(media);
@@ -102,31 +105,28 @@ public class FeedGridItemViewHolder extends RecyclerView.ViewHolder {
binding.typeIcon.setVisibility(View.VISIBLE);
binding.typeIcon.setImageResource(typeIconRes);
}
- final DownloadedCheckerAsyncTask task = new DownloadedCheckerAsyncTask(result -> {
- final List checkList = result.get(media.getPk());
- if (checkList == null || checkList.isEmpty()) {
- return;
- }
- switch (media.getMediaType()) {
- case MEDIA_TYPE_IMAGE:
- case MEDIA_TYPE_VIDEO:
- binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
- binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(R.color.green_A400)));
- break;
- case MEDIA_TYPE_SLIDER:
- binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
- final List carouselMedia = media.getCarouselMedia();
- boolean allDownloaded = checkList.size() == (carouselMedia == null ? 0 : carouselMedia.size());
- if (allDownloaded) {
- allDownloaded = checkList.stream().allMatch(downloaded -> downloaded);
- }
- binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(
- allDownloaded ? R.color.green_A400 : R.color.yellow_400)));
- break;
- default:
- }
- });
- task.execute(media);
+ final List checkList = DownloadUtils.checkDownloaded(media);
+ if (checkList.isEmpty()) {
+ return;
+ }
+ switch (media.getMediaType()) {
+ case MEDIA_TYPE_IMAGE:
+ case MEDIA_TYPE_VIDEO:
+ binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
+ binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(R.color.green_A400)));
+ break;
+ case MEDIA_TYPE_SLIDER:
+ binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
+ final List carouselMedia = media.getCarouselMedia();
+ boolean allDownloaded = checkList.size() == (carouselMedia == null ? 0 : carouselMedia.size());
+ if (allDownloaded) {
+ allDownloaded = checkList.stream().allMatch(downloaded -> downloaded);
+ }
+ binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(
+ allDownloaded ? R.color.green_A400 : R.color.yellow_400)));
+ break;
+ default:
+ }
}
private void setThumbImage(final String thumbnailUrl) {
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FilterViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FilterViewHolder.java
index 65637aea..b26ddad0 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/FilterViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FilterViewHolder.java
@@ -33,7 +33,7 @@ public class FilterViewHolder extends RecyclerView.ViewHolder {
this.binding = binding;
this.tuneFilters = tuneFilters;
this.onFilterClickListener = onFilterClickListener;
- appExecutors = AppExecutors.getInstance();
+ appExecutors = AppExecutors.INSTANCE;
}
public void bind(final int position, final String originalKey, final Bitmap originalBitmap, final Filter> item, final boolean isSelected) {
@@ -55,13 +55,13 @@ public class FilterViewHolder extends RecyclerView.ViewHolder {
final Bitmap bitmap = BitmapUtils.getBitmapFromMemCache(filterKey);
if (bitmap == null) {
final GPUImageFilter filter = item.getInstance();
- appExecutors.tasksThread().submit(() -> {
+ appExecutors.getTasksThread().submit(() -> {
GPUImage.getBitmapForMultipleFilters(
originalBitmap,
ImmutableList.builder().add(filter).addAll(tuneFilters).build(),
filteredBitmap -> {
BitmapUtils.addBitmapToMemoryCache(filterKey, filteredBitmap, true);
- appExecutors.mainThread().execute(() -> binding.getRoot().post(() -> binding.preview.setImageBitmap(filteredBitmap)));
+ appExecutors.getMainThread().execute(() -> binding.getRoot().post(() -> binding.preview.setImageBitmap(filteredBitmap)));
}
);
});
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java
index 6f48be53..1274bd2b 100755
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java
@@ -2,10 +2,9 @@ package awais.instagrabber.adapters.viewholder;
import android.view.View;
+import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
-import java.util.List;
-
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User;
@@ -14,23 +13,19 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder {
private final ItemFollowBinding binding;
- public FollowsViewHolder(final ItemFollowBinding binding) {
+ public FollowsViewHolder(@NonNull final ItemFollowBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final User model,
- final List admins,
final View.OnClickListener onClickListener) {
if (model == null) return;
itemView.setTag(model);
itemView.setOnClickListener(onClickListener);
- binding.tvUsername.setText(model.getUsername());
- binding.tvFullName.setText(model.getFullName());
- if (admins != null && admins.contains(model.getPk())) {
- binding.isAdmin.setVisibility(View.VISIBLE);
- }
- binding.ivProfilePic.setImageURI(model.getProfilePicUrl());
+ binding.username.setUsername("@" + model.getUsername(), model.isVerified());
+ binding.fullName.setText(model.getFullName());
+ binding.profilePic.setImageURI(model.getProfilePicUrl());
}
public void bind(final FollowModel model,
@@ -38,8 +33,8 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder {
if (model == null) return;
itemView.setTag(model);
itemView.setOnClickListener(onClickListener);
- binding.tvUsername.setText(model.getUsername());
- binding.tvFullName.setText(model.getFullName());
- binding.ivProfilePic.setImageURI(model.getProfilePicUrl());
+ binding.username.setUsername("@" + model.getUsername());
+ binding.fullName.setText(model.getFullName());
+ binding.profilePic.setImageURI(model.getProfilePicUrl());
}
}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java
new file mode 100644
index 00000000..4eb93fed
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java
@@ -0,0 +1,80 @@
+package awais.instagrabber.adapters.viewholder;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import awais.instagrabber.R;
+import awais.instagrabber.databinding.ItemSearchResultBinding;
+import awais.instagrabber.fragments.search.SearchCategoryFragment.OnSearchItemClickListener;
+import awais.instagrabber.models.enums.FavoriteType;
+import awais.instagrabber.repositories.responses.Hashtag;
+import awais.instagrabber.repositories.responses.Place;
+import awais.instagrabber.repositories.responses.User;
+import awais.instagrabber.repositories.responses.search.SearchItem;
+
+public class SearchItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemSearchResultBinding binding;
+ private final OnSearchItemClickListener onSearchItemClickListener;
+
+ public SearchItemViewHolder(@NonNull final ItemSearchResultBinding binding,
+ final OnSearchItemClickListener onSearchItemClickListener) {
+ super(binding.getRoot());
+ this.binding = binding;
+ this.onSearchItemClickListener = onSearchItemClickListener;
+ }
+
+ public void bind(final SearchItem searchItem) {
+ if (searchItem == null) return;
+ final FavoriteType type = searchItem.getType();
+ if (type == null) return;
+ String title;
+ String subtitle;
+ String picUrl;
+ boolean isVerified = false;
+ switch (type) {
+ case USER:
+ final User user = searchItem.getUser();
+ title = "@" + user.getUsername();
+ subtitle = user.getFullName();
+ picUrl = user.getProfilePicUrl();
+ isVerified = user.isVerified();
+ break;
+ case HASHTAG:
+ final Hashtag hashtag = searchItem.getHashtag();
+ title = "#" + hashtag.getName();
+ subtitle = hashtag.getSubtitle();
+ picUrl = "res:/" + R.drawable.ic_hashtag;
+ break;
+ case LOCATION:
+ final Place place = searchItem.getPlace();
+ title = place.getTitle();
+ subtitle = place.getSubtitle();
+ picUrl = "res:/" + R.drawable.ic_location;
+ break;
+ default:
+ return;
+ }
+ itemView.setOnClickListener(v -> {
+ if (onSearchItemClickListener != null) {
+ onSearchItemClickListener.onSearchItemClick(searchItem);
+ }
+ });
+ binding.delete.setVisibility(searchItem.isRecent() ? View.VISIBLE : View.GONE);
+ if (searchItem.isRecent()) {
+ binding.delete.setEnabled(true);
+ binding.delete.setOnClickListener(v -> {
+ if (onSearchItemClickListener != null) {
+ binding.delete.setEnabled(false);
+ onSearchItemClickListener.onSearchItemDelete(searchItem);
+ }
+ });
+ }
+ binding.title.setText(title);
+ binding.subtitle.setText(subtitle);
+ binding.profilePic.setImageURI(picUrl);
+ binding.verified.setVisibility(isVerified ? View.VISIBLE : View.GONE);
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java
index 37479a4f..914ee5e9 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java
@@ -2,7 +2,6 @@ package awais.instagrabber.adapters.viewholder;
import android.graphics.drawable.Animatable;
import android.net.Uri;
-import android.view.GestureDetector;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
@@ -14,8 +13,8 @@ import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import awais.instagrabber.adapters.SliderItemsAdapter;
-import awais.instagrabber.customviews.VerticalDragHelper;
import awais.instagrabber.customviews.drawee.AnimatedZoomableController;
+import awais.instagrabber.customviews.drawee.DoubleTapGestureListener;
import awais.instagrabber.databinding.ItemSliderPhotoBinding;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.ResponseBodyUtils;
@@ -24,13 +23,10 @@ public class SliderPhotoViewHolder extends SliderItemViewHolder {
private static final String TAG = "FeedSliderPhotoViewHolder";
private final ItemSliderPhotoBinding binding;
- private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener;
- public SliderPhotoViewHolder(@NonNull final ItemSliderPhotoBinding binding,
- final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener) {
+ public SliderPhotoViewHolder(@NonNull final ItemSliderPhotoBinding binding) {
super(binding.getRoot());
this.binding = binding;
- this.onVerticalDragListener = onVerticalDragListener;
}
public void bind(@NonNull final Media model,
@@ -62,74 +58,19 @@ public class SliderPhotoViewHolder extends SliderItemViewHolder {
})
.setLowResImageRequest(ImageRequest.fromUri(ResponseBodyUtils.getThumbUrl(model)))
.build());
- // binding.getRoot().setOnClickListener(v -> {
- // if (sliderCallback != null) {
- // sliderCallback.onItemClicked(position);
- // }
- // });
- binding.getRoot().setTapListener(new GestureDetector.SimpleOnGestureListener() {
+ final DoubleTapGestureListener tapListener = new DoubleTapGestureListener(binding.getRoot()) {
@Override
- public boolean onSingleTapUp(final MotionEvent e) {
+ public boolean onSingleTapConfirmed(final MotionEvent e) {
if (sliderCallback != null) {
- sliderCallback.onItemClicked(position);
- return true;
+ sliderCallback.onItemClicked(position, model, binding.getRoot());
}
- return false;
+ return super.onSingleTapConfirmed(e);
}
- });
+ };
+ binding.getRoot().setTapListener(tapListener);
final AnimatedZoomableController zoomableController = AnimatedZoomableController.newInstance();
zoomableController.setMaxScaleFactor(3f);
binding.getRoot().setZoomableController(zoomableController);
- if (onVerticalDragListener != null) {
- binding.getRoot().setOnVerticalDragListener(onVerticalDragListener);
- }
+ binding.getRoot().setZoomingEnabled(true);
}
-
- // private void setDimensions(final FeedModel feedModel, final int spanCount, final boolean animate) {
- // final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams();
- // final int deviceWidth = Utils.displayMetrics.widthPixels;
- // final int spanWidth = deviceWidth / spanCount;
- // final int spanHeight = NumberUtils.getResultingHeight(spanWidth, feedModel.getImageHeight(), feedModel.getImageWidth());
- // final int width = spanWidth == 0 ? deviceWidth : spanWidth;
- // final int height = spanHeight == 0 ? deviceWidth + 1 : spanHeight;
- // if (animate) {
- // Animation animation = AnimationUtils.expand(
- // binding.imageViewer,
- // layoutParams.width,
- // layoutParams.height,
- // width,
- // height,
- // new Animation.AnimationListener() {
- // @Override
- // public void onAnimationStart(final Animation animation) {
- // showOrHideDetails(spanCount);
- // }
- //
- // @Override
- // public void onAnimationEnd(final Animation animation) {
- // // showOrHideDetails(spanCount);
- // }
- //
- // @Override
- // public void onAnimationRepeat(final Animation animation) {
- //
- // }
- // });
- // binding.imageViewer.startAnimation(animation);
- // } else {
- // layoutParams.width = width;
- // layoutParams.height = height;
- // binding.imageViewer.requestLayout();
- // }
- // }
- //
- // private void showOrHideDetails(final int spanCount) {
- // if (spanCount == 1) {
- // binding.itemFeedTop.getRoot().setVisibility(View.VISIBLE);
- // binding.itemFeedBottom.getRoot().setVisibility(View.VISIBLE);
- // } else {
- // binding.itemFeedTop.getRoot().setVisibility(View.GONE);
- // binding.itemFeedBottom.getRoot().setVisibility(View.GONE);
- // }
- // }
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java
index c62cf711..8f6226ef 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java
@@ -7,17 +7,17 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
+import com.google.android.exoplayer2.ui.StyledPlayerView;
+
import java.util.List;
import awais.instagrabber.adapters.SliderItemsAdapter;
-import awais.instagrabber.customviews.VerticalDragHelper;
import awais.instagrabber.customviews.VideoPlayerCallbackAdapter;
import awais.instagrabber.customviews.VideoPlayerViewHelper;
-import awais.instagrabber.databinding.LayoutExoCustomControlsBinding;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
+import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.VideoVersion;
-import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
@@ -28,40 +28,23 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
private static final String TAG = "SliderVideoViewHolder";
private final LayoutVideoPlayerWithThumbnailBinding binding;
- private final LayoutExoCustomControlsBinding controlsBinding;
private final boolean loadVideoOnItemClick;
- private final GestureDetector.OnGestureListener videoPlayerViewGestureListener = new GestureDetector.SimpleOnGestureListener() {
- @Override
- public boolean onSingleTapConfirmed(final MotionEvent e) {
- binding.playerView.performClick();
- return true;
- }
- };
private VideoPlayerViewHelper videoPlayerViewHelper;
@SuppressLint("ClickableViewAccessibility")
public SliderVideoViewHolder(@NonNull final LayoutVideoPlayerWithThumbnailBinding binding,
- final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener,
- final LayoutExoCustomControlsBinding controlsBinding,
final boolean loadVideoOnItemClick) {
super(binding.getRoot());
this.binding = binding;
- this.controlsBinding = controlsBinding;
this.loadVideoOnItemClick = loadVideoOnItemClick;
- // if (onVerticalDragListener != null) {
- // final VerticalDragHelper thumbnailVerticalDragHelper = new VerticalDragHelper(binding.thumbnailParent);
- // final VerticalDragHelper playerVerticalDragHelper = new VerticalDragHelper(binding.playerView);
- // thumbnailVerticalDragHelper.setOnVerticalDragListener(onVerticalDragListener);
- // playerVerticalDragHelper.setOnVerticalDragListener(onVerticalDragListener);
- // binding.thumbnailParent.setOnTouchListener((v, event) -> {
- // final boolean onDragTouch = thumbnailVerticalDragHelper.onDragTouch(event);
- // if (onDragTouch) {
- // return true;
- // }
- // return thumbnailVerticalDragHelper.onGestureTouchEvent(event);
- // });
- // }
+ final GestureDetector.OnGestureListener videoPlayerViewGestureListener = new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapConfirmed(final MotionEvent e) {
+ binding.playerView.performClick();
+ return true;
+ }
+ };
final GestureDetector gestureDetector = new GestureDetector(itemView.getContext(), videoPlayerViewGestureListener);
binding.playerView.setOnTouchListener((v, event) -> {
gestureDetector.onTouchEvent(event);
@@ -72,13 +55,13 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
public void bind(@NonNull final Media media,
final int position,
final SliderItemsAdapter.SliderCallback sliderCallback) {
- final float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f;
+ final float vol = settingsHelper.getBoolean(PreferenceKeys.MUTED_VIDEOS) ? 0f : 1f;
final VideoPlayerViewHelper.VideoPlayerCallback videoPlayerCallback = new VideoPlayerCallbackAdapter() {
@Override
public void onThumbnailClick() {
if (sliderCallback != null) {
- sliderCallback.onItemClicked(position);
+ sliderCallback.onItemClicked(position, media, binding.getRoot());
}
}
@@ -121,6 +104,21 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
sliderCallback.onPlayerRelease(position);
}
}
+
+ @Override
+ public void onFullScreenModeChanged(final boolean isFullScreen, final StyledPlayerView playerView) {
+ if (sliderCallback != null) {
+ sliderCallback.onFullScreenModeChanged(isFullScreen, playerView);
+ }
+ }
+
+ @Override
+ public boolean isInFullScreen() {
+ if (sliderCallback != null) {
+ return sliderCallback.isInFullScreen();
+ }
+ return false;
+ }
};
final float aspectRatio = (float) media.getOriginalWidth() / media.getOriginalHeight();
String videoUrl = null;
@@ -139,16 +137,10 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
aspectRatio,
ResponseBodyUtils.getThumbUrl(media),
loadVideoOnItemClick,
- controlsBinding,
videoPlayerCallback);
- // binding.itemFeedBottom.btnMute.setOnClickListener(v -> {
- // final float newVol = videoPlayerViewHelper.toggleMute();
- // setMuteIcon(newVol);
- // Utils.sessionVolumeFull = newVol == 1f;
- // });
binding.playerView.setOnClickListener(v -> {
if (sliderCallback != null) {
- sliderCallback.onItemClicked(position);
+ sliderCallback.onItemClicked(position, media, binding.getRoot());
}
});
}
@@ -162,62 +154,4 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
if (videoPlayerViewHelper == null) return;
videoPlayerViewHelper.releasePlayer();
}
-
- public void resetPlayerTimeline() {
- if (videoPlayerViewHelper == null) return;
- videoPlayerViewHelper.resetTimeline();
- }
-
- public void removeCallbacks() {
- if (videoPlayerViewHelper == null) return;
- videoPlayerViewHelper.removeCallbacks();
- }
-
- // private void setDimensions(final FeedModel feedModel, final int spanCount, final boolean animate) {
- // final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams();
- // final int deviceWidth = Utils.displayMetrics.widthPixels;
- // final int spanWidth = deviceWidth / spanCount;
- // final int spanHeight = NumberUtils.getResultingHeight(spanWidth, feedModel.getImageHeight(), feedModel.getImageWidth());
- // final int width = spanWidth == 0 ? deviceWidth : spanWidth;
- // final int height = spanHeight == 0 ? deviceWidth + 1 : spanHeight;
- // if (animate) {
- // Animation animation = AnimationUtils.expand(
- // binding.imageViewer,
- // layoutParams.width,
- // layoutParams.height,
- // width,
- // height,
- // new Animation.AnimationListener() {
- // @Override
- // public void onAnimationStart(final Animation animation) {
- // showOrHideDetails(spanCount);
- // }
- //
- // @Override
- // public void onAnimationEnd(final Animation animation) {
- // // showOrHideDetails(spanCount);
- // }
- //
- // @Override
- // public void onAnimationRepeat(final Animation animation) {
- //
- // }
- // });
- // binding.imageViewer.startAnimation(animation);
- // } else {
- // layoutParams.width = width;
- // layoutParams.height = height;
- // binding.imageViewer.requestLayout();
- // }
- // }
- //
- // private void showOrHideDetails(final int spanCount) {
- // if (spanCount == 1) {
- // binding.itemFeedTop.getRoot().setVisibility(View.VISIBLE);
- // binding.itemFeedBottom.getRoot().setVisibility(View.VISIBLE);
- // } else {
- // binding.itemFeedTop.getRoot().setVisibility(View.GONE);
- // binding.itemFeedBottom.getRoot().setVisibility(View.GONE);
- // }
- // }
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java
index 9633e549..a421e98c 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java
@@ -20,7 +20,6 @@ public final class StoryListViewHolder extends RecyclerView.ViewHolder {
}
public void bind(final FeedStoryModel model,
- final int position,
final OnFeedStoryClickListener notificationClickListener) {
if (model == null) return;
@@ -53,7 +52,7 @@ public final class StoryListViewHolder extends RecyclerView.ViewHolder {
itemView.setOnClickListener(v -> {
if (notificationClickListener == null) return;
- notificationClickListener.onFeedStoryClick(model, position);
+ notificationClickListener.onFeedStoryClick(model);
});
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java
index 86b0d35b..809e09b1 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java
@@ -54,12 +54,14 @@ public class TopicClusterViewHolder extends RecyclerView.ViewHolder {
if (onTopicClickListener != null) {
itemView.setOnClickListener(v -> onTopicClickListener.onTopicClick(
topicCluster,
- binding.getRoot(),
binding.cover,
- binding.title,
titleColor.get(),
backgroundColor.get()
));
+ itemView.setOnLongClickListener(v -> {
+ onTopicClickListener.onTopicLongClick(topicCluster.getCoverMedia());
+ return true;
+ });
}
// binding.title.setTransitionName("title-" + topicCluster.getId());
binding.cover.setTransitionName("cover-" + topicCluster.getId());
@@ -126,11 +128,11 @@ public class TopicClusterViewHolder extends RecyclerView.ViewHolder {
backgroundColor.get()
));
}
- // binding.title.setTransitionName("title-" + topicCluster.getId());
- binding.cover.setTransitionName("cover-" + topicCluster.getId());
- final String thumbUrl = ResponseBodyUtils.getThumbUrl(topicCluster.getCoverMedias() == null
+ // binding.title.setTransitionName("title-" + topicCluster.getCollectionId());
+ binding.cover.setTransitionName("cover-" + topicCluster.getCollectionId());
+ final String thumbUrl = ResponseBodyUtils.getThumbUrl(topicCluster.getCoverMediaList() == null
? topicCluster.getCoverMedia()
- : topicCluster.getCoverMedias().get(0));
+ : topicCluster.getCoverMediaList().get(0));
if (thumbUrl == null) {
binding.cover.setImageURI((String) null);
} else {
@@ -172,6 +174,6 @@ public class TopicClusterViewHolder extends RecyclerView.ViewHolder {
}, CallerThreadExecutor.getInstance());
binding.cover.setImageRequest(imageRequest);
}
- binding.title.setText(topicCluster.getTitle());
+ binding.title.setText(topicCluster.getCollectionName());
}
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java
deleted file mode 100644
index fd4cfb12..00000000
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package awais.instagrabber.adapters.viewholder.comments;
-
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-import awais.instagrabber.R;
-import awais.instagrabber.adapters.CommentsAdapter.CommentCallback;
-import awais.instagrabber.databinding.ItemCommentSmallBinding;
-import awais.instagrabber.models.CommentModel;
-import awais.instagrabber.repositories.responses.User;
-import awais.instagrabber.utils.Utils;
-
-public final class ChildCommentViewHolder extends RecyclerView.ViewHolder {
-
- private final ItemCommentSmallBinding binding;
-
- public ChildCommentViewHolder(@NonNull final ItemCommentSmallBinding binding) {
- super(binding.getRoot());
- this.binding = binding;
- }
-
- public void bind(final CommentModel comment,
- final boolean selected,
- final CommentCallback commentCallback) {
- if (comment == null) return;
- if (commentCallback != null) {
- itemView.setOnClickListener(v -> commentCallback.onClick(comment));
- }
- if (selected) {
- itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_selected));
- } else {
- itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent));
- }
- setupCommentText(comment, commentCallback);
- binding.tvDate.setText(comment.getDateTime());
- setLiked(comment.getLiked());
- setLikes((int) comment.getLikes());
- setUser(comment);
- }
-
- private void setupCommentText(final CommentModel comment, final CommentCallback commentCallback) {
- binding.tvComment.clearOnURLClickListeners();
- binding.tvComment.clearOnHashtagClickListeners();
- binding.tvComment.clearOnMentionClickListeners();
- binding.tvComment.clearOnEmailClickListeners();
- binding.tvComment.setText(comment.getText());
- binding.tvComment.addOnHashtagListener(autoLinkItem -> {
- final String originalText = autoLinkItem.getOriginalText();
- if (commentCallback == null) return;
- commentCallback.onHashtagClick(originalText);
- });
- binding.tvComment.addOnMentionClickListener(autoLinkItem -> {
- final String originalText = autoLinkItem.getOriginalText();
- if (commentCallback == null) return;
- commentCallback.onMentionClick(originalText);
-
- });
- binding.tvComment.addOnEmailClickListener(autoLinkItem -> {
- final String originalText = autoLinkItem.getOriginalText();
- if (commentCallback == null) return;
- commentCallback.onEmailClick(originalText);
- });
- binding.tvComment.addOnURLClickListener(autoLinkItem -> {
- final String originalText = autoLinkItem.getOriginalText();
- if (commentCallback == null) return;
- commentCallback.onURLClick(originalText);
- });
- binding.tvComment.setOnLongClickListener(v -> {
- Utils.copyText(itemView.getContext(), comment.getText());
- return true;
- });
- binding.tvComment.setOnClickListener(v -> commentCallback.onClick(comment));
- }
-
- private void setUser(final CommentModel comment) {
- final User profileModel = comment.getProfileModel();
- if (profileModel == null) return;
- binding.tvUsername.setText(profileModel.getUsername());
- binding.ivProfilePic.setImageURI(profileModel.getProfilePicUrl());
- binding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE);
- }
-
- private void setLikes(final int likes) {
- final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes);
- binding.tvLikes.setText(likesString);
- }
-
- public final void setLiked(final boolean liked) {
- if (liked) {
- itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_liked));
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java
deleted file mode 100644
index 31edac56..00000000
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package awais.instagrabber.adapters.viewholder.comments;
-
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-import awais.instagrabber.R;
-import awais.instagrabber.adapters.CommentsAdapter.CommentCallback;
-import awais.instagrabber.databinding.ItemCommentBinding;
-import awais.instagrabber.models.CommentModel;
-import awais.instagrabber.repositories.responses.User;
-import awais.instagrabber.utils.Utils;
-
-public final class ParentCommentViewHolder extends RecyclerView.ViewHolder {
-
- private final ItemCommentBinding binding;
-
- public ParentCommentViewHolder(@NonNull final ItemCommentBinding binding) {
- super(binding.getRoot());
- this.binding = binding;
- }
-
- public void bind(final CommentModel comment,
- final boolean selected,
- final CommentCallback commentCallback) {
- if (comment == null) return;
- if (commentCallback != null) {
- itemView.setOnClickListener(v -> commentCallback.onClick(comment));
- }
- if (selected) {
- itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_selected));
- } else {
- itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent));
- }
- setupCommentText(comment, commentCallback);
- binding.tvDate.setText(comment.getDateTime());
- setLiked(comment.getLiked());
- setLikes((int) comment.getLikes());
- setUser(comment);
- }
-
- private void setupCommentText(final CommentModel comment, final CommentCallback commentCallback) {
- binding.tvComment.clearOnURLClickListeners();
- binding.tvComment.clearOnHashtagClickListeners();
- binding.tvComment.clearOnMentionClickListeners();
- binding.tvComment.clearOnEmailClickListeners();
- binding.tvComment.setText(comment.getText());
- binding.tvComment.addOnHashtagListener(autoLinkItem -> {
- final String originalText = autoLinkItem.getOriginalText();
- if (commentCallback == null) return;
- commentCallback.onHashtagClick(originalText);
- });
- binding.tvComment.addOnMentionClickListener(autoLinkItem -> {
- final String originalText = autoLinkItem.getOriginalText();
- if (commentCallback == null) return;
- commentCallback.onMentionClick(originalText);
-
- });
- binding.tvComment.addOnEmailClickListener(autoLinkItem -> {
- final String originalText = autoLinkItem.getOriginalText();
- if (commentCallback == null) return;
- commentCallback.onEmailClick(originalText);
- });
- binding.tvComment.addOnURLClickListener(autoLinkItem -> {
- final String originalText = autoLinkItem.getOriginalText();
- if (commentCallback == null) return;
- commentCallback.onURLClick(originalText);
- });
- binding.tvComment.setOnLongClickListener(v -> {
- Utils.copyText(itemView.getContext(), comment.getText());
- return true;
- });
- binding.tvComment.setOnClickListener(v -> commentCallback.onClick(comment));
- }
-
- private void setUser(final CommentModel comment) {
- final User profileModel = comment.getProfileModel();
- if (profileModel == null) return;
- binding.tvUsername.setText(profileModel.getUsername());
- binding.ivProfilePic.setImageURI(profileModel.getProfilePicUrl());
- binding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE);
- }
-
- private void setLikes(final int likes) {
- final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes);
- binding.tvLikes.setText(likesString);
- }
-
- public final void setLiked(final boolean liked) {
- if (liked) {
- itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_liked));
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/dialogs/KeywordsFilterDialogViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/dialogs/KeywordsFilterDialogViewHolder.java
index 65fe7b77..9930be59 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/dialogs/KeywordsFilterDialogViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/dialogs/KeywordsFilterDialogViewHolder.java
@@ -14,7 +14,7 @@ import java.util.HashSet;
import awais.instagrabber.R;
import awais.instagrabber.adapters.KeywordsFilterAdapter;
-import awais.instagrabber.utils.Constants;
+import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.utils.SettingsHelper;
public class KeywordsFilterDialogViewHolder extends RecyclerView.ViewHolder {
@@ -34,7 +34,7 @@ public class KeywordsFilterDialogViewHolder extends RecyclerView.ViewHolder {
final String s = items.get(position);
SettingsHelper settingsHelper = new SettingsHelper(context);
items.remove(position);
- settingsHelper.putStringSet(Constants.KEYWORD_FILTERS, new HashSet<>(items));
+ settingsHelper.putStringSet(PreferenceKeys.KEYWORD_FILTERS, new HashSet<>(items));
adapter.notifyDataSetChanged();
final String message = context.getString(R.string.removed_keywords, s);
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java
index 2755cf32..2b0596b6 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java
@@ -17,11 +17,9 @@ import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectMessageInboxAdapter.OnItemClickListener;
import awais.instagrabber.databinding.LayoutDmInboxItemBinding;
-import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
-import awais.instagrabber.repositories.responses.directmessages.DirectThreadDirectStory;
import awais.instagrabber.utils.DMUtils;
import awais.instagrabber.utils.TextUtils;
@@ -136,7 +134,7 @@ public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder {
private void setDateTime(@NonNull final DirectItem item) {
final long timestamp = item.getTimestamp() / 1000;
- final String dateTimeString = TextUtils.getRelativeDateTimeString(itemView.getContext(), timestamp);
+ final String dateTimeString = TextUtils.getRelativeDateTimeString(timestamp);
binding.tvDate.setText(dateTimeString);
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemActionLogViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemActionLogViewHolder.java
index 45c83805..c212efbf 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemActionLogViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemActionLogViewHolder.java
@@ -22,6 +22,7 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemActionLog;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.repositories.responses.directmessages.TextRange;
import awais.instagrabber.utils.TextUtils;
public class DirectItemActionLogViewHolder extends DirectItemViewHolder {
@@ -45,16 +46,16 @@ public class DirectItemActionLogViewHolder extends DirectItemViewHolder {
final DirectItemActionLog actionLog = directItemModel.getActionLog();
final String text = actionLog.getDescription();
final SpannableStringBuilder sb = new SpannableStringBuilder(text);
- final List bold = actionLog.getBold();
+ final List bold = actionLog.getBold();
if (bold != null && !bold.isEmpty()) {
- for (final DirectItemActionLog.TextRange textRange : bold) {
+ for (final TextRange textRange : bold) {
final StyleSpan boldStyleSpan = new StyleSpan(Typeface.BOLD);
sb.setSpan(boldStyleSpan, textRange.getStart(), textRange.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
}
- final List textAttributes = actionLog.getTextAttributes();
+ final List textAttributes = actionLog.getTextAttributes();
if (textAttributes != null && !textAttributes.isEmpty()) {
- for (final DirectItemActionLog.TextRange textAttribute : textAttributes) {
+ for (final TextRange textAttribute : textAttributes) {
if (!TextUtils.isEmpty(textAttribute.getColor())) {
final ForegroundColorSpan colorSpan = new ForegroundColorSpan(itemView.getResources().getColor(R.color.deep_orange_400));
sb.setSpan(colorSpan, textAttribute.getStart(), textAttribute.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java
index 7bc5d173..5b3b1e33 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java
@@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.backends.pipeline.Fresco;
@@ -23,6 +22,7 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemAnimatedMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.Utils;
@@ -48,7 +48,7 @@ public class DirectItemAnimatedMediaViewHolder extends DirectItemViewHolder {
final AnimatedMediaFixedHeight fixedHeight = images.getFixedHeight();
if (fixedHeight == null) return;
final String url = fixedHeight.getWebp();
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
fixedHeight.getHeight(),
fixedHeight.getWidth(),
mediaImageMaxHeight,
@@ -56,8 +56,8 @@ public class DirectItemAnimatedMediaViewHolder extends DirectItemViewHolder {
);
binding.ivAnimatedMessage.setVisibility(View.VISIBLE);
final ViewGroup.LayoutParams layoutParams = binding.ivAnimatedMessage.getLayoutParams();
- final int width = widthHeight.first != null ? widthHeight.first : 0;
- final int height = widthHeight.second != null ? widthHeight.second : 0;
+ final int width = widthHeight.first;
+ final int height = widthHeight.second;
layoutParams.width = width;
layoutParams.height = height;
binding.ivAnimatedMessage.requestLayout();
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
index dddd9315..82c07ab7 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
@@ -6,7 +6,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.drawable.ScalingUtils;
@@ -31,6 +30,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemClip;
import awais.instagrabber.repositories.responses.directmessages.DirectItemFelixShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
@@ -103,15 +103,15 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.setRoundingParams(roundingParams)
.build());
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.mediaPreview.getLayoutParams();
- layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
- layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
+ layoutParams.width = widthHeight.first;
+ layoutParams.height = widthHeight.second;
binding.mediaPreview.requestLayout();
binding.mediaPreview.setTag(url);
binding.mediaPreview.setImageURI(url);
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
index 24a9e62e..769548a8 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
@@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
@@ -14,11 +13,11 @@ import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmMediaBinding;
import awais.instagrabber.models.enums.MediaItemType;
-import awais.instagrabber.repositories.responses.ImageVersions2;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
@@ -53,16 +52,16 @@ public class DirectItemMediaViewHolder extends DirectItemViewHolder {
binding.typeIcon.setVisibility(modelMediaType == MediaItemType.MEDIA_TYPE_VIDEO || modelMediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE
: View.GONE);
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.mediaPreview.getLayoutParams();
- final int width = widthHeight.first != null ? widthHeight.first : 0;
+ final int width = widthHeight.first;
layoutParams.width = width;
- layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
+ layoutParams.height = widthHeight.second;
binding.mediaPreview.requestLayout();
binding.bgTime.getLayoutParams().width = width;
binding.bgTime.requestLayout();
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
index 6198faf1..9778d046 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
@@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
@@ -21,6 +20,7 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
@@ -170,15 +170,15 @@ public class DirectItemRavenMediaViewHolder extends DirectItemViewHolder {
binding.typeIcon.setVisibility(modelMediaType == MediaItemType.MEDIA_TYPE_VIDEO || modelMediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE
: View.GONE);
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
maxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.preview.getLayoutParams();
- layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
- layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
+ layoutParams.width = widthHeight.first;
+ layoutParams.height = widthHeight.second;
binding.preview.requestLayout();
final String thumbUrl = ResponseBodyUtils.getThumbUrl(media);
binding.preview.setImageURI(thumbUrl);
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
index b6b7bf68..45a6a8d4 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
@@ -5,7 +5,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.drawable.ScalingUtils;
@@ -17,12 +16,12 @@ import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmStoryShareBinding;
import awais.instagrabber.models.enums.MediaItemType;
-import awais.instagrabber.repositories.responses.ImageVersions2;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
@@ -76,15 +75,15 @@ public class DirectItemStoryShareViewHolder extends DirectItemViewHolder {
.setRoundingParams(roundingParams)
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.build());
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
storyShareMedia.getOriginalHeight(),
storyShareMedia.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.ivMediaPreview.getLayoutParams();
- layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
- layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
+ layoutParams.width = widthHeight.first;
+ layoutParams.height = widthHeight.second;
binding.ivMediaPreview.requestLayout();
final String thumbUrl = ResponseBodyUtils.getThumbUrl(storyShareMedia);
binding.ivMediaPreview.setImageURI(thumbUrl);
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVideoCallEventViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVideoCallEventViewHolder.java
index 77a43a5a..4c56922a 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVideoCallEventViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVideoCallEventViewHolder.java
@@ -17,9 +17,9 @@ import awais.instagrabber.databinding.LayoutDmActionLogBinding;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
-import awais.instagrabber.repositories.responses.directmessages.DirectItemActionLog;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVideoCallEvent;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.repositories.responses.directmessages.TextRange;
import awais.instagrabber.utils.TextUtils;
public class DirectItemVideoCallEventViewHolder extends DirectItemViewHolder {
@@ -41,9 +41,9 @@ public class DirectItemVideoCallEventViewHolder extends DirectItemViewHolder {
final DirectItemVideoCallEvent videoCallEvent = directItemModel.getVideoCallEvent();
final String text = videoCallEvent.getDescription();
final SpannableStringBuilder sb = new SpannableStringBuilder(text);
- final List textAttributes = videoCallEvent.getTextAttributes();
+ final List textAttributes = videoCallEvent.getTextAttributes();
if (textAttributes != null && !textAttributes.isEmpty()) {
- for (final DirectItemActionLog.TextRange textAttribute : textAttributes) {
+ for (final TextRange textAttribute : textAttributes) {
if (!TextUtils.isEmpty(textAttribute.getColor())) {
final ForegroundColorSpan colorSpan = new ForegroundColorSpan(itemView.getResources().getColor(R.color.deep_orange_400));
sb.setSpan(colorSpan, textAttribute.getStart(), textAttribute.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
index 17a0956c..610acc7c 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
@@ -5,7 +5,6 @@ import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
-import android.text.format.DateFormat;
import android.view.Gravity;
import android.view.View;
import android.view.ViewConfiguration;
@@ -25,6 +24,9 @@ import androidx.transition.TransitionManager;
import com.google.android.material.transition.MaterialFade;
import com.google.common.collect.ImmutableList;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
@@ -144,7 +146,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
}
setupReply(item, messageDirection);
setReactions(item, position);
- if (item.getRepliedToMessage() == null && item.showForwardAttribution()) {
+ if (item.getRepliedToMessage() == null && item.getShowForwardAttribution()) {
setForwardInfo(messageDirection);
}
}
@@ -163,7 +165,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
binding.ivProfilePic.setVisibility(messageDirection == MessageDirection.INCOMING && thread.isGroup() ? View.VISIBLE : View.GONE);
binding.tvUsername.setVisibility(messageDirection == MessageDirection.INCOMING && thread.isGroup() ? View.VISIBLE : View.GONE);
if (messageDirection == MessageDirection.INCOMING && thread.isGroup()) {
- final User user = getUser(item.getUserId(), thread.getUsers());
+ final List allUsers = new LinkedList(thread.getUsers());
+ allUsers.addAll(thread.getLeftUsers());
+ final User user = getUser(item.getUserId(), allUsers);
if (user != null) {
binding.tvUsername.setText(user.getUsername());
binding.ivProfilePic.setImageURI(user.getProfilePicUrl());
@@ -193,7 +197,10 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
if (showMessageInfo()) {
binding.messageInfo.setVisibility(View.VISIBLE);
binding.deliveryStatus.setVisibility(messageDirection == MessageDirection.OUTGOING ? View.VISIBLE : View.GONE);
- binding.messageTime.setText(DateFormat.getTimeFormat(itemView.getContext()).format(item.getDate()));
+ if (item.getDate() != null) {
+ final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
+ binding.messageTime.setText(dateFormatter.format(item.getDate()));
+ }
if (messageDirection == MessageDirection.OUTGOING) {
if (item.isPending()) {
binding.deliveryStatus.setImageResource(R.drawable.ic_check_24);
@@ -216,7 +223,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
private void setupReply(final DirectItem item, final MessageDirection messageDirection) {
if (item.getRepliedToMessage() != null) {
- setReply(item, messageDirection, thread.getUsers());
+ final List allUsers = new LinkedList(thread.getUsers());
+ allUsers.addAll(thread.getLeftUsers());
+ setReply(item, messageDirection, allUsers);
} else {
binding.quoteLine.setVisibility(View.GONE);
binding.replyContainer.setVisibility(View.GONE);
@@ -551,6 +560,10 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
menu.setOnDismissListener(() -> setSelected(false));
menu.setOnReactionClickListener(emoji -> callback.onReaction(item, emoji));
menu.setOnOptionSelectListener((itemId, cb) -> callback.onOptionSelect(item, itemId, cb));
+ menu.setOnAddReactionListener(() -> {
+ menu.dismiss();
+ itemView.postDelayed(() -> callback.onAddReactionListener(item), 300);
+ });
menu.show(itemView, location);
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java
index 1b1a9fda..d6420bda 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java
@@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.backends.pipeline.Fresco;
@@ -16,6 +15,8 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemXma;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.repositories.responses.directmessages.XmaUrlInfo;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
public class DirectItemXmaViewHolder extends DirectItemViewHolder {
@@ -35,15 +36,15 @@ public class DirectItemXmaViewHolder extends DirectItemViewHolder {
@Override
public void bindItem(final DirectItem item, final MessageDirection messageDirection) {
final DirectItemXma xma = item.getXma();
- final DirectItemXma.XmaUrlInfo playableUrlInfo = xma.getPlayableUrlInfo();
- final DirectItemXma.XmaUrlInfo previewUrlInfo = xma.getPreviewUrlInfo();
+ final XmaUrlInfo playableUrlInfo = xma.getPlayableUrlInfo();
+ final XmaUrlInfo previewUrlInfo = xma.getPreviewUrlInfo();
if (playableUrlInfo == null && previewUrlInfo == null) {
binding.ivAnimatedMessage.setController(null);
return;
}
- final DirectItemXma.XmaUrlInfo urlInfo = playableUrlInfo != null ? playableUrlInfo : previewUrlInfo;
+ final XmaUrlInfo urlInfo = playableUrlInfo != null ? playableUrlInfo : previewUrlInfo;
final String url = urlInfo.getUrl();
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
urlInfo.getHeight(),
urlInfo.getWidth(),
mediaImageMaxHeight,
@@ -51,8 +52,8 @@ public class DirectItemXmaViewHolder extends DirectItemViewHolder {
);
binding.ivAnimatedMessage.setVisibility(View.VISIBLE);
final ViewGroup.LayoutParams layoutParams = binding.ivAnimatedMessage.getLayoutParams();
- final int width = widthHeight.first != null ? widthHeight.first : 0;
- final int height = widthHeight.second != null ? widthHeight.second : 0;
+ final int width = widthHeight.first;
+ final int height = widthHeight.second;
layoutParams.width = width;
layoutParams.height = height;
binding.ivAnimatedMessage.requestLayout();
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectReactionViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectReactionViewHolder.java
index 497f7a06..84ee907d 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectReactionViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectReactionViewHolder.java
@@ -31,7 +31,7 @@ public class DirectReactionViewHolder extends RecyclerView.ViewHolder {
this.onReactionClickListener = onReactionClickListener;
binding.info.setVisibility(View.GONE);
binding.secondaryImage.setVisibility(View.VISIBLE);
- emojiParser = EmojiParser.getInstance();
+ emojiParser = EmojiParser.Companion.getInstance(itemView.getContext());
}
public void bind(final DirectItemEmojiReaction reaction,
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/RecipientThreadViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/RecipientThreadViewHolder.java
index aab59a58..eb39f405 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/RecipientThreadViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/RecipientThreadViewHolder.java
@@ -37,7 +37,7 @@ public class RecipientThreadViewHolder extends RecyclerView.ViewHolder {
final DirectThread thread,
final boolean showSelection,
final boolean isSelected) {
- if (thread == null) return;
+ if (thread == null || thread.getUsers().size() == 0) return;
binding.getRoot().setOnClickListener(v -> {
if (onThreadClickListener == null) return;
onThreadClickListener.onClick(position, RankedRecipient.of(thread), isSelected);
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java
index 860bd027..042264c9 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java
@@ -55,7 +55,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
private void setupComments(@NonNull final Media feedModel) {
final long commentsCount = feedModel.getCommentCount();
bottomBinding.commentsCount.setText(String.valueOf(commentsCount));
- bottomBinding.commentsCount.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel));
+ bottomBinding.btnComments.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel));
}
private void setupProfilePic(@NonNull final Media media) {
@@ -75,6 +75,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
// final SpannableString spannableString = new SpannableString();
// spannableString.setSpan(new CommentMentionClickSpan(), 0, titleLen, 0);
final User user = media.getUser();
+ if (user == null) return;
final String title = "@" + user.getUsername();
topBinding.title.setText(title);
topBinding.title.setOnClickListener(v -> feedItemCallback.onNameClick(media, topBinding.ivProfilePic));
@@ -120,8 +121,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT
));
- }
- else {
+ } else {
final String locationName = location.getName();
if (TextUtils.isEmpty(locationName)) {
topBinding.location.setVisibility(View.GONE);
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
index b9271d6f..6931c851 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
@@ -45,9 +45,9 @@ public class FeedSliderViewHolder extends FeedItemViewHolder {
final String text = "1/" + sliderItemLen;
binding.mediaCounter.setText(text);
binding.mediaList.setOffscreenPageLimit(1);
- final SliderItemsAdapter adapter = new SliderItemsAdapter(null, null, false, new SliderCallbackAdapter() {
+ final SliderItemsAdapter adapter = new SliderItemsAdapter(false, new SliderCallbackAdapter() {
@Override
- public void onItemClicked(final int position) {
+ public void onItemClicked(final int position, final Media media, final View view) {
feedItemCallback.onSliderClick(feedModel, position);
}
});
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java
index 33ae8757..e94af79b 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java
@@ -18,9 +18,9 @@ import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.customviews.VideoPlayerCallbackAdapter;
import awais.instagrabber.customviews.VideoPlayerViewHelper;
import awais.instagrabber.databinding.ItemFeedVideoBinding;
+import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.VideoVersion;
-import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
@@ -65,7 +65,7 @@ public class FeedVideoViewHolder extends FeedItemViewHolder {
// Log.d(TAG, "Binding post: " + feedModel.getPostId());
this.media = media;
binding.itemFeedBottom.tvVideoViews.setText(String.valueOf(media.getViewCount()));
- final float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f;
+ final float vol = settingsHelper.getBoolean(PreferenceKeys.MUTED_VIDEOS) ? 0f : 1f;
final VideoPlayerViewHelper.VideoPlayerCallback videoPlayerCallback = new VideoPlayerCallbackAdapter() {
@Override
@@ -97,7 +97,7 @@ public class FeedVideoViewHolder extends FeedItemViewHolder {
aspectRatio,
ResponseBodyUtils.getThumbUrl(media),
false,
- null,
+ // null,
videoPlayerCallback);
binding.videoPost.thumbnail.post(() -> {
if (media.getOriginalHeight() > 0.8 * Utils.displayMetrics.heightPixels) {
diff --git a/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java
deleted file mode 100755
index b75b758c..00000000
--- a/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java
+++ /dev/null
@@ -1,268 +0,0 @@
-package awais.instagrabber.asyncs;
-
-import android.os.AsyncTask;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
-
-import awais.instagrabber.BuildConfig;
-import awais.instagrabber.interfaces.FetchListener;
-import awais.instagrabber.models.CommentModel;
-import awais.instagrabber.repositories.responses.FriendshipStatus;
-import awais.instagrabber.repositories.responses.User;
-import awais.instagrabber.utils.Constants;
-import awais.instagrabber.utils.NetworkUtils;
-import awais.instagrabber.utils.TextUtils;
-//import awaisomereport.LogCollector;
-
-//import static awais.instagrabber.utils.Utils.logCollector;
-
-public final class CommentsFetcher extends AsyncTask> {
- private static final String TAG = "CommentsFetcher";
-
- private final String shortCode, endCursor;
- private final FetchListener> fetchListener;
-
- public CommentsFetcher(final String shortCode, final String endCursor, final FetchListener> fetchListener) {
- this.shortCode = shortCode;
- this.endCursor = endCursor;
- this.fetchListener = fetchListener;
- }
-
- @NonNull
- @Override
- protected List doInBackground(final Void... voids) {
- /*
- "https://www.instagram.com/graphql/query/?query_hash=97b41c52301f77ce508f55e66d17620e&variables=" + "{\"shortcode\":\"" + shortcode + "\",\"first\":50,\"after\":\"" + endCursor + "\"}";
-
- 97b41c52301f77ce508f55e66d17620e -> for comments
- 51fdd02b67508306ad4484ff574a0b62 -> for child comments
-
- https://www.instagram.com/graphql/query/?query_hash=51fdd02b67508306ad4484ff574a0b62&variables={"comment_id":"18100041898085322","first":50,"after":""}
- */
- final List commentModels = getParentComments();
- if (commentModels != null) {
- for (final CommentModel commentModel : commentModels) {
- final List childCommentModels = commentModel.getChildCommentModels();
- if (childCommentModels != null) {
- final int childCommentsLen = childCommentModels.size();
- final CommentModel lastChild = childCommentModels.get(childCommentsLen - 1);
- if (lastChild != null && lastChild.hasNextPage() && !TextUtils.isEmpty(lastChild.getEndCursor())) {
- final List remoteChildComments = getChildComments(commentModel.getId());
- commentModel.setChildCommentModels(remoteChildComments);
- lastChild.setPageCursor(false, null);
- }
- }
- }
- }
- return commentModels;
- }
-
- @Override
- protected void onPreExecute() {
- if (fetchListener != null) fetchListener.doBefore();
- }
-
- @Override
- protected void onPostExecute(final List result) {
- if (fetchListener != null) fetchListener.onResult(result);
- }
-
- @NonNull
- private synchronized List getChildComments(final String commentId) {
- final List commentModels = new ArrayList<>();
- String childEndCursor = "";
- while (childEndCursor != null) {
- final String url = "https://www.instagram.com/graphql/query/?query_hash=51fdd02b67508306ad4484ff574a0b62&variables=" +
- "{\"comment_id\":\"" + commentId + "\",\"first\":50,\"after\":\"" + childEndCursor + "\"}";
- try {
- final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
- conn.setUseCaches(false);
- conn.connect();
-
- if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) break;
- else {
- final JSONObject data = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("data")
- .getJSONObject("comment")
- .getJSONObject("edge_threaded_comments");
-
- final JSONObject pageInfo = data.getJSONObject("page_info");
- childEndCursor = pageInfo.getString("end_cursor");
- if (TextUtils.isEmpty(childEndCursor)) childEndCursor = null;
-
- final JSONArray childComments = data.optJSONArray("edges");
- if (childComments != null) {
- final int length = childComments.length();
- for (int i = 0; i < length; ++i) {
- final JSONObject childComment = childComments.getJSONObject(i).optJSONObject("node");
-
- if (childComment != null) {
- final JSONObject owner = childComment.getJSONObject("owner");
- final User user = new User(
- owner.optLong(Constants.EXTRAS_ID, 0),
- owner.getString(Constants.EXTRAS_USERNAME),
- null,
- false,
- owner.getString("profile_pic_url"),
- null,
- new FriendshipStatus(false, false, false, false, false, false, false, false, false, false),
- false, false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null,
- null, null, null);
- final JSONObject likedBy = childComment.optJSONObject("edge_liked_by");
- commentModels.add(new CommentModel(childComment.getString(Constants.EXTRAS_ID),
- childComment.getString("text"),
- childComment.getLong("created_at"),
- likedBy != null ? likedBy.optLong("count", 0) : 0,
- childComment.getBoolean("viewer_has_liked"),
- user));
- }
- }
- }
- }
- conn.disconnect();
- } catch (final Exception e) {
-// if (logCollector != null)
-// logCollector.appendException(e,
-// LogCollector.LogFile.ASYNC_COMMENTS_FETCHER,
-// "getChildComments",
-// new Pair<>("commentModels.size", commentModels.size()));
- if (BuildConfig.DEBUG) Log.e(TAG, "", e);
- if (fetchListener != null) fetchListener.onFailure(e);
- break;
- }
- }
-
- return commentModels;
- }
-
- @NonNull
- private synchronized List getParentComments() {
- final List commentModels = new ArrayList<>();
- final String url = "https://www.instagram.com/graphql/query/?query_hash=bc3296d1ce80a24b1b6e40b1e72903f5&variables=" +
- "{\"shortcode\":\"" + shortCode + "\",\"first\":50,\"after\":\"" + endCursor.replace("\"", "\\\"") + "\"}";
- try {
- final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
- conn.setUseCaches(false);
- conn.connect();
-
- if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) return null;
- else {
- final JSONObject parentComments = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("data")
- .getJSONObject("shortcode_media")
- .getJSONObject(
- "edge_media_to_parent_comment");
-
- final JSONObject pageInfo = parentComments.getJSONObject("page_info");
- final String foundEndCursor = pageInfo.optString("end_cursor");
- final boolean hasNextPage = pageInfo.optBoolean("has_next_page", !TextUtils.isEmpty(foundEndCursor));
-
- // final boolean containsToken = endCursor.contains("bifilter_token");
- // if (!Utils.isEmpty(endCursor) && (containsToken || endCursor.contains("cached_comments_cursor"))) {
- // final JSONObject endCursorObject = new JSONObject(endCursor);
- // endCursor = endCursorObject.optString("cached_comments_cursor");
- //
- // if (!Utils.isEmpty(endCursor))
- // endCursor = "{\\\"cached_comments_cursor\\\": \\\"" + endCursor + "\\\", ";
- // else
- // endCursor = "{";
- //
- // endCursor = endCursor + "\\\"bifilter_token\\\": \\\"" + endCursorObject.getString("bifilter_token") + "\\\"}";
- // }
- // else if (containsToken) endCursor = null;
-
- final JSONArray comments = parentComments.getJSONArray("edges");
- final int commentsLen = comments.length();
- for (int i = 0; i < commentsLen; ++i) {
- final JSONObject comment = comments.getJSONObject(i).getJSONObject("node");
-
- final JSONObject owner = comment.getJSONObject("owner");
- final User user = new User(
- owner.optLong(Constants.EXTRAS_ID, 0),
- owner.getString(Constants.EXTRAS_USERNAME),
- null,
- false,
- owner.getString("profile_pic_url"),
- null,
- new FriendshipStatus(false, false, false, false, false, false, false, false, false, false),
- owner.optBoolean("is_verified"),
- false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null, null,
- null, null);
- final JSONObject likedBy = comment.optJSONObject("edge_liked_by");
- final String commentId = comment.getString(Constants.EXTRAS_ID);
- final CommentModel commentModel = new CommentModel(commentId,
- comment.getString("text"),
- comment.getLong("created_at"),
- likedBy != null ? likedBy.optLong("count", 0) : 0,
- comment.getBoolean("viewer_has_liked"),
- user);
- if (i == 0 && !foundEndCursor.contains("tao_cursor"))
- commentModel.setPageCursor(hasNextPage, TextUtils.isEmpty(foundEndCursor) ? null : foundEndCursor);
- JSONObject tempJsonObject;
- final JSONArray childCommentsArray;
- final int childCommentsLen;
- if ((tempJsonObject = comment.optJSONObject("edge_threaded_comments")) != null &&
- (childCommentsArray = tempJsonObject.optJSONArray("edges")) != null
- && (childCommentsLen = childCommentsArray.length()) > 0) {
-
- final String childEndCursor;
- final boolean childHasNextPage;
- if ((tempJsonObject = tempJsonObject.optJSONObject("page_info")) != null) {
- childEndCursor = tempJsonObject.optString("end_cursor");
- childHasNextPage = tempJsonObject.optBoolean("has_next_page", !TextUtils.isEmpty(childEndCursor));
- } else {
- childEndCursor = null;
- childHasNextPage = false;
- }
-
- final List childCommentModels = new ArrayList<>();
- for (int j = 0; j < childCommentsLen; ++j) {
- final JSONObject childComment = childCommentsArray.getJSONObject(j).getJSONObject("node");
-
- tempJsonObject = childComment.getJSONObject("owner");
- final User childUser = new User(
- tempJsonObject.optLong(Constants.EXTRAS_ID, 0),
- tempJsonObject.getString(Constants.EXTRAS_USERNAME),
- null,
- false,
- tempJsonObject.getString("profile_pic_url"),
- null,
- new FriendshipStatus(false, false, false, false, false, false, false, false, false, false),
- tempJsonObject.optBoolean("is_verified"), false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0,
- null, null, null, null, null, null);
-
- tempJsonObject = childComment.optJSONObject("edge_liked_by");
- childCommentModels.add(new CommentModel(childComment.getString(Constants.EXTRAS_ID),
- childComment.getString("text"),
- childComment.getLong("created_at"),
- tempJsonObject != null ? tempJsonObject.optLong("count", 0) : 0,
- childComment.getBoolean("viewer_has_liked"),
- childUser));
- }
- childCommentModels.get(childCommentsLen - 1).setPageCursor(childHasNextPage, childEndCursor);
- commentModel.setChildCommentModels(childCommentModels);
- }
- commentModels.add(commentModel);
- }
- }
-
- conn.disconnect();
- } catch (final Exception e) {
-// if (logCollector != null)
-// logCollector.appendException(e, LogCollector.LogFile.ASYNC_COMMENTS_FETCHER, "getParentComments",
-// new Pair<>("commentModelsList.size", commentModels.size()));
- if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
- if (fetchListener != null) fetchListener.onFailure(e);
- return null;
- }
- return commentModels;
- }
-}
diff --git a/app/src/main/java/awais/instagrabber/asyncs/CreateThreadAction.java b/app/src/main/java/awais/instagrabber/asyncs/CreateThreadAction.java
deleted file mode 100644
index 384b3bbd..00000000
--- a/app/src/main/java/awais/instagrabber/asyncs/CreateThreadAction.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package awais.instagrabber.asyncs;
-
-import android.os.AsyncTask;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Locale;
-
-import awais.instagrabber.repositories.responses.directmessages.DirectThread;
-import awais.instagrabber.utils.Constants;
-import awais.instagrabber.utils.CookieUtils;
-import awais.instagrabber.utils.Utils;
-import awais.instagrabber.webservices.DirectMessagesService;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-public class CreateThreadAction extends AsyncTask {
- private static final String TAG = "CommentAction";
-
- private final String cookie;
- private final long userId;
- private final OnTaskCompleteListener onTaskCompleteListener;
- private final DirectMessagesService directMessagesService;
-
- public CreateThreadAction(final String cookie, final long userId, final OnTaskCompleteListener onTaskCompleteListener) {
- this.cookie = cookie;
- this.userId = userId;
- this.onTaskCompleteListener = onTaskCompleteListener;
- directMessagesService = DirectMessagesService.getInstance(CookieUtils.getCsrfTokenFromCookie(cookie),
- CookieUtils.getUserIdFromCookie(cookie),
- Utils.settingsHelper.getString(Constants.DEVICE_UUID));
- }
-
- protected Void doInBackground(Void... lmao) {
- final Call createThreadRequest = directMessagesService.createThread(Collections.singletonList(userId), null);
- createThreadRequest.enqueue(new Callback() {
- @Override
- public void onResponse(@NonNull final Call call, @NonNull final Response response) {
- if (!response.isSuccessful()) {
- if (response.errorBody() != null) {
- try {
- final String string = response.errorBody().string();
- final String msg = String.format(Locale.US,
- "onResponse: url: %s, responseCode: %d, errorBody: %s",
- call.request().url().toString(),
- response.code(),
- string);
- Log.e(TAG, msg);
- } catch (IOException e) {
- Log.e(TAG, "onResponse: ", e);
- }
- }
- Log.e(TAG, "onResponse: request was not successful and response error body was null");
- }
- onTaskCompleteListener.onTaskComplete(response.body());
- if (response.body() == null) {
- Log.e(TAG, "onResponse: thread is null");
- }
- }
-
- @Override
- public void onFailure(@NonNull final Call call, @NonNull final Throwable t) {
- onTaskCompleteListener.onTaskComplete(null);
- }
- });
- return null;
- }
-
-// @Override
-// protected void onPostExecute() {
-// }
-
- public interface OnTaskCompleteListener {
- void onTaskComplete(final DirectThread thread);
- }
-}
diff --git a/app/src/main/java/awais/instagrabber/asyncs/DownloadedCheckerAsyncTask.java b/app/src/main/java/awais/instagrabber/asyncs/DownloadedCheckerAsyncTask.java
deleted file mode 100644
index 605bed3f..00000000
--- a/app/src/main/java/awais/instagrabber/asyncs/DownloadedCheckerAsyncTask.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package awais.instagrabber.asyncs;
-
-import android.os.AsyncTask;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import awais.instagrabber.repositories.responses.Media;
-import awais.instagrabber.utils.DownloadUtils;
-
-public final class DownloadedCheckerAsyncTask extends AsyncTask>> {
- private static final String TAG = "DownloadedCheckerAsyncTask";
-
- private final OnCheckResultListener listener;
-
- public DownloadedCheckerAsyncTask(final OnCheckResultListener listener) {
- this.listener = listener;
- }
-
- @Override
- protected Map> doInBackground(final Media... feedModels) {
- if (feedModels == null) {
- return null;
- }
- final Map> map = new HashMap<>();
- for (final Media media : feedModels) {
- map.put(media.getPk(), DownloadUtils.checkDownloaded(media));
- }
- return map;
- }
-
- @Override
- protected void onPostExecute(final Map> result) {
- if (listener == null) return;
- listener.onResult(result);
- }
-
- public interface OnCheckResultListener {
- void onResult(final Map> result);
- }
-}
diff --git a/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java
index fa9f331e..87b38bb7 100644
--- a/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java
+++ b/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java
@@ -7,13 +7,15 @@ import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Hashtag;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
-import awais.instagrabber.webservices.GraphQLService;
+import awais.instagrabber.utils.CoroutineUtilsKt;
+import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.TagsService;
+import kotlinx.coroutines.Dispatchers;
public class HashtagPostFetchService implements PostFetcher.PostFetchService {
private final TagsService tagsService;
- private final GraphQLService graphQLService;
+ private final GraphQLRepository graphQLRepository;
private final Hashtag hashtagModel;
private String nextMaxId;
private boolean moreAvailable;
@@ -23,7 +25,7 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService {
this.hashtagModel = hashtagModel;
this.isLoggedIn = isLoggedIn;
tagsService = isLoggedIn ? TagsService.getInstance() : null;
- graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
+ graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
}
@Override
@@ -48,7 +50,17 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService {
}
};
if (isLoggedIn) tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb);
- else graphQLService.fetchHashtagPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb);
+ else graphQLRepository.fetchHashtagPosts(
+ hashtagModel.getName().toLowerCase(),
+ nextMaxId,
+ CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
+ if (throwable != null) {
+ cb.onFailure(throwable);
+ return;
+ }
+ cb.onSuccess(postsFetchResponse);
+ }, Dispatchers.getIO())
+ );
}
@Override
diff --git a/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java
index 274b2314..31b9f90c 100644
--- a/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java
+++ b/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java
@@ -7,13 +7,15 @@ import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
-import awais.instagrabber.webservices.GraphQLService;
+import awais.instagrabber.utils.CoroutineUtilsKt;
+import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.LocationService;
import awais.instagrabber.webservices.ServiceCallback;
+import kotlinx.coroutines.Dispatchers;
public class LocationPostFetchService implements PostFetcher.PostFetchService {
private final LocationService locationService;
- private final GraphQLService graphQLService;
+ private final GraphQLRepository graphQLRepository;
private final Location locationModel;
private String nextMaxId;
private boolean moreAvailable;
@@ -23,7 +25,7 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService {
this.locationModel = locationModel;
this.isLoggedIn = isLoggedIn;
locationService = isLoggedIn ? LocationService.getInstance() : null;
- graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
+ graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
}
@Override
@@ -48,7 +50,17 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService {
}
};
if (isLoggedIn) locationService.fetchPosts(locationModel.getPk(), nextMaxId, cb);
- else graphQLService.fetchLocationPosts(locationModel.getPk(), nextMaxId, cb);
+ else graphQLRepository.fetchLocationPosts(
+ locationModel.getPk(),
+ nextMaxId,
+ CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
+ if (throwable != null) {
+ cb.onFailure(throwable);
+ return;
+ }
+ cb.onSuccess(postsFetchResponse);
+ }, Dispatchers.getIO())
+ );
}
@Override
diff --git a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java
deleted file mode 100755
index ddd670f1..00000000
--- a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java
+++ /dev/null
@@ -1,160 +0,0 @@
-package awais.instagrabber.asyncs;
-
-import android.os.AsyncTask;
-import android.util.Log;
-
-import org.json.JSONObject;
-
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-import awais.instagrabber.interfaces.FetchListener;
-import awais.instagrabber.repositories.responses.Media;
-import awais.instagrabber.utils.NetworkUtils;
-import awais.instagrabber.utils.ResponseBodyUtils;
-//import awaisomereport.LogCollector;
-
-//import static awais.instagrabber.utils.Utils.logCollector;
-
-public final class PostFetcher extends AsyncTask {
- private static final String TAG = "PostFetcher";
-
- private final String shortCode;
- private final FetchListener fetchListener;
-
- public PostFetcher(final String shortCode, final FetchListener fetchListener) {
- this.shortCode = shortCode;
- this.fetchListener = fetchListener;
- }
-
- @Override
- protected Media doInBackground(final Void... voids) {
- HttpURLConnection conn = null;
- try {
- conn = (HttpURLConnection) new URL("https://www.instagram.com/p/" + shortCode + "/?__a=1").openConnection();
- conn.setUseCaches(false);
- conn.connect();
-
- if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
-
- final JSONObject media = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("graphql")
- .getJSONObject("shortcode_media");
- // ProfileModel profileModel = null;
- // if (media.has("owner")) {
- // final JSONObject owner = media.getJSONObject("owner");
- // profileModel = new ProfileModel(
- // owner.optBoolean("is_private"),
- // owner.optBoolean("is_private"),
- // owner.optBoolean("is_verified"),
- // owner.optString("id"),
- // owner.optString("username"),
- // owner.optString("full_name"),
- // null,
- // null,
- // owner.optString("profile_pic_url"),
- // owner.optString("profile_pic_url"),
- // owner.optInt("edge_owner_to_timeline_media"),
- // owner.optInt("edge_followed_by"),
- // -1,
- // owner.optBoolean("followed_by_viewer"),
- // false,
- // owner.optBoolean("restricted_by_viewer"),
- // owner.optBoolean("blocked_by_viewer"),
- // owner.optBoolean("requested_by_viewer")
- // );
- // }
- // final long timestamp = media.getLong("taken_at_timestamp");
- //
- // final boolean isVideo = media.has("is_video") && media.optBoolean("is_video");
- // final boolean isSlider = media.has("edge_sidecar_to_children");
- //
- // final MediaItemType mediaItemType;
- // if (isSlider) mediaItemType = MediaItemType.MEDIA_TYPE_SLIDER;
- // else if (isVideo) mediaItemType = MediaItemType.MEDIA_TYPE_VIDEO;
- // else mediaItemType = MediaItemType.MEDIA_TYPE_IMAGE;
- //
- // final String postCaption;
- // final JSONObject mediaToCaption = media.optJSONObject("edge_media_to_caption");
- // if (mediaToCaption == null) postCaption = null;
- // else {
- // final JSONArray captions = mediaToCaption.optJSONArray("edges");
- // postCaption = captions != null && captions.length() > 0 ?
- // captions.getJSONObject(0).getJSONObject("node").optString("text") : null;
- // }
- //
- // JSONObject commentObject = media.optJSONObject("edge_media_to_parent_comment");
- // final long commentsCount = commentObject != null ? commentObject.optLong("count") : 0;
- // final FeedModel.Builder feedModelBuilder = new FeedModel.Builder()
- // .setItemType(mediaItemType)
- // .setPostId(media.getString(Constants.EXTRAS_ID))
- // .setDisplayUrl(isVideo ? media.getString("video_url")
- // : ResponseBodyUtils.getHighQualityImage(media))
- // .setThumbnailUrl(media.getString("display_url"))
- // .setImageHeight(media.getJSONObject("dimensions").getInt("height"))
- // .setImageWidth(media.getJSONObject("dimensions").getInt("width"))
- // .setShortCode(shortCode)
- // .setPostCaption(TextUtils.isEmpty(postCaption) ? null : postCaption)
- // .setProfileModel(profileModel)
- // .setViewCount(isVideo && media.has("video_view_count")
- // ? media.getLong("video_view_count")
- // : -1)
- // .setTimestamp(timestamp)
- // .setLiked(media.getBoolean("viewer_has_liked"))
- // .setBookmarked(media.getBoolean("viewer_has_saved"))
- // .setLikesCount(media.getJSONObject("edge_media_preview_like")
- // .getLong("count"))
- // .setLocationName(media.isNull("location")
- // ? null
- // : media.getJSONObject("location").optString("name"))
- // .setLocationId(media.isNull("location")
- // ? null
- // : media.getJSONObject("location").optString("id"))
- // .setCommentsCount(commentsCount);
- // if (isSlider) {
- // final JSONArray children = media.getJSONObject("edge_sidecar_to_children").getJSONArray("edges");
- // final List postModels = new ArrayList<>();
- // for (int i = 0; i < children.length(); ++i) {
- // final JSONObject childNode = children.getJSONObject(i).getJSONObject("node");
- // final boolean isChildVideo = childNode.getBoolean("is_video");
- // postModels.add(new PostChild.Builder()
- // .setItemType(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO
- // : MediaItemType.MEDIA_TYPE_IMAGE)
- // .setDisplayUrl(isChildVideo ? childNode.getString("video_url")
- // : childNode.getString("display_url"))
- // .setShortCode(media.getString(Constants.EXTRAS_SHORTCODE))
- // .setVideoViews(isChildVideo && childNode.has("video_view_count")
- // ? childNode.getLong("video_view_count")
- // : -1)
- // .setThumbnailUrl(childNode.getString("display_url"))
- // .setHeight(childNode.getJSONObject("dimensions").getInt("height"))
- // .setWidth(childNode.getJSONObject("dimensions").getInt("width"))
- // .build());
- // }
- // feedModelBuilder.setSliderItems(postModels);
- // }
- // return feedModelBuilder.build();
- return ResponseBodyUtils.parseGraphQLItem(media, null);
- }
- } catch (Exception e) {
-// if (logCollector != null) {
-// logCollector.appendException(e, LogCollector.LogFile.ASYNC_POST_FETCHER, "doInBackground");
-// }
- Log.e(TAG, "Error fetching post", e);
- } finally {
- if (conn != null) {
- conn.disconnect();
- }
- }
- return null;
- }
-
- @Override
- protected void onPreExecute() {
- if (fetchListener != null) fetchListener.doBefore();
- }
-
- @Override
- protected void onPostExecute(final Media feedModel) {
- if (fetchListener != null) fetchListener.onResult(feedModel);
- }
-}
diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java
index 02e0e27a..2c609dcb 100644
--- a/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java
+++ b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java
@@ -7,14 +7,16 @@ import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.repositories.responses.User;
-import awais.instagrabber.webservices.GraphQLService;
+import awais.instagrabber.utils.CoroutineUtilsKt;
+import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.ProfileService;
import awais.instagrabber.webservices.ServiceCallback;
+import kotlinx.coroutines.Dispatchers;
public class ProfilePostFetchService implements PostFetcher.PostFetchService {
private static final String TAG = "ProfilePostFetchService";
private final ProfileService profileService;
- private final GraphQLService graphQLService;
+ private final GraphQLRepository graphQLRepository;
private final User profileModel;
private final boolean isLoggedIn;
private String nextMaxId;
@@ -23,7 +25,7 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService {
public ProfilePostFetchService(final User profileModel, final boolean isLoggedIn) {
this.profileModel = profileModel;
this.isLoggedIn = isLoggedIn;
- graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
+ graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
profileService = isLoggedIn ? ProfileService.getInstance() : null;
}
@@ -49,7 +51,19 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService {
}
};
if (isLoggedIn) profileService.fetchPosts(profileModel.getPk(), nextMaxId, cb);
- else graphQLService.fetchProfilePosts(profileModel.getPk(), 30, nextMaxId, profileModel, cb);
+ else graphQLRepository.fetchProfilePosts(
+ profileModel.getPk(),
+ 30,
+ nextMaxId,
+ profileModel,
+ CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
+ if (throwable != null) {
+ cb.onFailure(throwable);
+ return;
+ }
+ cb.onSuccess(postsFetchResponse);
+ }, Dispatchers.getIO())
+ );
}
@Override
diff --git a/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java
index 9b3511a9..a84f960a 100644
--- a/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java
+++ b/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java
@@ -7,13 +7,15 @@ import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.enums.PostItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
-import awais.instagrabber.webservices.GraphQLService;
+import awais.instagrabber.utils.CoroutineUtilsKt;
+import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.ProfileService;
import awais.instagrabber.webservices.ServiceCallback;
+import kotlinx.coroutines.Dispatchers;
public class SavedPostFetchService implements PostFetcher.PostFetchService {
private final ProfileService profileService;
- private final GraphQLService graphQLService;
+ private final GraphQLRepository graphQLRepository;
private final long profileId;
private final PostItemType type;
private final boolean isLoggedIn;
@@ -27,7 +29,7 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService {
this.type = type;
this.isLoggedIn = isLoggedIn;
this.collectionId = collectionId;
- graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
+ graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
profileService = isLoggedIn ? ProfileService.getInstance() : null;
}
@@ -58,7 +60,18 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService {
break;
case TAGGED:
if (isLoggedIn) profileService.fetchTagged(profileId, nextMaxId, callback);
- else graphQLService.fetchTaggedPosts(profileId, 30, nextMaxId, callback);
+ else graphQLRepository.fetchTaggedPosts(
+ profileId,
+ 30,
+ nextMaxId,
+ CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
+ if (throwable != null) {
+ callback.onFailure(throwable);
+ return;
+ }
+ callback.onSuccess(postsFetchResponse);
+ }, Dispatchers.getIO())
+ );
break;
case COLLECTION:
case SAVED:
diff --git a/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java b/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
index 1731dbf3..c0568a70 100644
--- a/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
+++ b/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
@@ -105,12 +105,15 @@ public class ChatMessageLayout extends FrameLayout {
viewPartMainLastLineWidth = viewPartMainLineCount > 0
? ((TextView) firstChild).getLayout().getLineWidth(viewPartMainLineCount - 1)
: 0;
+ // also include start left padding
+ viewPartMainLastLineWidth += firstChild.getPaddingLeft();
}
- if (viewPartMainLineCount > 1 && !(viewPartMainLastLineWidth + viewPartInfoWidth > viewPartMain.getMeasuredWidth())) {
+ final float lastLineWithInfoWidth = viewPartMainLastLineWidth + viewPartInfoWidth;
+ if (viewPartMainLineCount > 1 && lastLineWithInfoWidth <= viewPartMain.getMeasuredWidth()) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight;
- } else if (viewPartMainLineCount > 1 && (viewPartMainLastLineWidth + viewPartInfoWidth > availableWidth)) {
+ } else if (viewPartMainLineCount > 1 && (lastLineWithInfoWidth > availableWidth)) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight + viewPartInfoHeight;
} else if (viewPartMainLineCount == 1 && (viewPartMainWidth + viewPartInfoWidth > availableWidth)) {
@@ -120,6 +123,16 @@ public class ChatMessageLayout extends FrameLayout {
heightSize += viewPartMainHeight;
widthSize += viewPartMainWidth + viewPartInfoWidth;
}
+
+ // if (isInEditMode()) {
+ // TextView wDebugView = (TextView) ((ViewGroup) this.getParent()).findViewWithTag("debug");
+ // wDebugView.setText(lastLineWithInfoWidth
+ // + "\n" + availableWidth
+ // + "\n" + viewPartMain.getMeasuredWidth()
+ // + "\n" + (lastLineWithInfoWidth <= viewPartMain.getMeasuredWidth())
+ // + "\n" + (lastLineWithInfoWidth > availableWidth)
+ // + "\n" + (viewPartMainWidth + viewPartInfoWidth > availableWidth));
+ // }
}
setMeasuredDimension(widthSize, heightSize);
super.onMeasure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));
diff --git a/app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java b/app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java
index 65a18141..b13a3a47 100644
--- a/app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java
+++ b/app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java
@@ -80,7 +80,7 @@ public class DirectItemContextMenu extends PopupWindow {
if (!showReactions && (options == null || options.isEmpty())) {
throw new IllegalArgumentException("showReactions is set false and options are empty");
}
- reactionsManager = ReactionsManager.getInstance();
+ reactionsManager = ReactionsManager.getInstance(context);
final Resources resources = context.getResources();
emojiSize = resources.getDimensionPixelSize(R.dimen.reaction_picker_emoji_size);
emojiMargin = resources.getDimensionPixelSize(R.dimen.reaction_picker_emoji_margin);
diff --git a/app/src/main/java/awais/instagrabber/customviews/FormattedNumberTextView.java b/app/src/main/java/awais/instagrabber/customviews/FormattedNumberTextView.java
new file mode 100644
index 00000000..06cdbf93
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/FormattedNumberTextView.java
@@ -0,0 +1,165 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.transition.ChangeBounds;
+import androidx.transition.Transition;
+import androidx.transition.TransitionManager;
+import androidx.transition.TransitionSet;
+
+import java.time.Duration;
+
+import awais.instagrabber.customviews.helpers.ChangeText;
+import awais.instagrabber.utils.NumberUtils;
+
+public class FormattedNumberTextView extends AppCompatTextView {
+ private static final String TAG = FormattedNumberTextView.class.getSimpleName();
+ private static final Transition TRANSITION;
+
+ private long number = Long.MIN_VALUE;
+ private boolean showAbbreviation = true;
+ private boolean animateChanges = false;
+ private boolean toggleOnClick = true;
+ private boolean autoToggleToAbbreviation = true;
+ private long autoToggleTimeoutMs = Duration.ofSeconds(2).toMillis();
+ private boolean initDone = false;
+
+ static {
+ final TransitionSet transitionSet = new TransitionSet();
+ final ChangeText changeText = new ChangeText().setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN);
+ transitionSet.addTransition(changeText).addTransition(new ChangeBounds());
+ TRANSITION = transitionSet;
+ }
+
+
+ public FormattedNumberTextView(@NonNull final Context context) {
+ super(context);
+ init();
+ }
+
+ public FormattedNumberTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public FormattedNumberTextView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ if (initDone) return;
+ setupClickToggle();
+ initDone = true;
+ }
+
+ private void setupClickToggle() {
+ setOnClickListener(null);
+ }
+
+ private OnClickListener getWrappedClickListener(@Nullable final OnClickListener l) {
+ if (!toggleOnClick) {
+ return l;
+ }
+ return v -> {
+ toggleAbbreviation();
+ if (l != null) {
+ l.onClick(this);
+ }
+ };
+ }
+
+ public void setNumber(final long number) {
+ if (this.number == number) return;
+ this.number = number;
+ format();
+ }
+
+ public void clearNumber() {
+ if (number == Long.MIN_VALUE) return;
+ number = Long.MIN_VALUE;
+ format();
+ }
+
+ public void setShowAbbreviation(final boolean showAbbreviation) {
+ if (this.showAbbreviation && showAbbreviation) return;
+ this.showAbbreviation = showAbbreviation;
+ format();
+ }
+
+ public boolean isShowAbbreviation() {
+ return showAbbreviation;
+ }
+
+ private void toggleAbbreviation() {
+ if (number == Long.MIN_VALUE) return;
+ setShowAbbreviation(!showAbbreviation);
+ }
+
+ public void setToggleOnClick(final boolean toggleOnClick) {
+ this.toggleOnClick = toggleOnClick;
+ }
+
+ public boolean isToggleOnClick() {
+ return toggleOnClick;
+ }
+
+ public void setAutoToggleToAbbreviation(final boolean autoToggleToAbbreviation) {
+ this.autoToggleToAbbreviation = autoToggleToAbbreviation;
+ }
+
+ public boolean isAutoToggleToAbbreviation() {
+ return autoToggleToAbbreviation;
+ }
+
+ public void setAutoToggleTimeoutMs(final long autoToggleTimeoutMs) {
+ this.autoToggleTimeoutMs = autoToggleTimeoutMs;
+ }
+
+ public long getAutoToggleTimeoutMs() {
+ return autoToggleTimeoutMs;
+ }
+
+ public void setAnimateChanges(final boolean animateChanges) {
+ this.animateChanges = animateChanges;
+ }
+
+ public boolean isAnimateChanges() {
+ return animateChanges;
+ }
+
+ @Override
+ public void setOnClickListener(@Nullable final OnClickListener l) {
+ super.setOnClickListener(getWrappedClickListener(l));
+ }
+
+ private void format() {
+ post(() -> {
+ if (animateChanges) {
+ try {
+ TransitionManager.beginDelayedTransition((ViewGroup) getParent(), TRANSITION);
+ } catch (Exception e) {
+ Log.e(TAG, "format: ", e);
+ }
+ }
+ if (number == Long.MIN_VALUE) {
+ setText(null);
+ return;
+ }
+ if (showAbbreviation) {
+ setText(NumberUtils.abbreviate(number, null));
+ return;
+ }
+ setText(String.valueOf(number));
+ if (autoToggleToAbbreviation) {
+ getHandler().postDelayed(() -> setShowAbbreviation(true), autoToggleTimeoutMs);
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/FragmentNavigatorWithDefaultAnimations.java b/app/src/main/java/awais/instagrabber/customviews/FragmentNavigatorWithDefaultAnimations.java
new file mode 100644
index 00000000..358e34d8
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/FragmentNavigatorWithDefaultAnimations.java
@@ -0,0 +1,75 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentManager;
+import androidx.navigation.NavDestination;
+import androidx.navigation.NavOptions;
+import androidx.navigation.Navigator;
+import androidx.navigation.fragment.FragmentNavigator;
+
+import awais.instagrabber.R;
+
+@Navigator.Name("fragment")
+public class FragmentNavigatorWithDefaultAnimations extends FragmentNavigator {
+
+ private final NavOptions emptyNavOptions = new NavOptions.Builder().build();
+ // private final NavOptions defaultNavOptions = new NavOptions.Builder()
+ // .setEnterAnim(R.animator.nav_default_enter_anim)
+ // .setExitAnim(R.animator.nav_default_exit_anim)
+ // .setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
+ // .setPopExitAnim(R.animator.nav_default_pop_exit_anim)
+ // .build();
+
+ private final NavOptions defaultNavOptions = new NavOptions.Builder()
+ .setEnterAnim(R.anim.slide_in_right)
+ .setExitAnim(R.anim.slide_out_left)
+ .setPopEnterAnim(android.R.anim.slide_in_left)
+ .setPopExitAnim(android.R.anim.slide_out_right)
+ .build();
+
+ public FragmentNavigatorWithDefaultAnimations(@NonNull final Context context,
+ @NonNull final FragmentManager manager,
+ final int containerId) {
+ super(context, manager, containerId);
+ }
+
+ @Nullable
+ @Override
+ public NavDestination navigate(@NonNull final Destination destination,
+ @Nullable final Bundle args,
+ @Nullable final NavOptions navOptions,
+ @Nullable final Navigator.Extras navigatorExtras) {
+ // this will try to fill in empty animations with defaults when no shared element transitions are set
+ // https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element
+ final boolean shouldUseTransitionsInstead = navigatorExtras != null;
+ final NavOptions navOptions1 = shouldUseTransitionsInstead ? navOptions : fillEmptyAnimationsWithDefaults(navOptions);
+ return super.navigate(destination, args, navOptions1, navigatorExtras);
+ }
+
+ private NavOptions fillEmptyAnimationsWithDefaults(@Nullable final NavOptions navOptions) {
+ if (navOptions == null) {
+ return defaultNavOptions;
+ }
+ return copyNavOptionsWithDefaultAnimations(navOptions);
+ }
+
+ @NonNull
+ private NavOptions copyNavOptionsWithDefaultAnimations(@NonNull final NavOptions navOptions) {
+ return new NavOptions.Builder()
+ .setLaunchSingleTop(navOptions.shouldLaunchSingleTop())
+ .setPopUpTo(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive())
+ .setEnterAnim(navOptions.getEnterAnim() == emptyNavOptions.getEnterAnim()
+ ? defaultNavOptions.getEnterAnim() : navOptions.getEnterAnim())
+ .setExitAnim(navOptions.getExitAnim() == emptyNavOptions.getExitAnim()
+ ? defaultNavOptions.getExitAnim() : navOptions.getExitAnim())
+ .setPopEnterAnim(navOptions.getPopEnterAnim() == emptyNavOptions.getPopEnterAnim()
+ ? defaultNavOptions.getPopEnterAnim() : navOptions.getPopEnterAnim())
+ .setPopExitAnim(navOptions.getPopExitAnim() == emptyNavOptions.getPopExitAnim()
+ ? defaultNavOptions.getPopExitAnim() : navOptions.getPopExitAnim())
+ .build();
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java b/app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java
new file mode 100644
index 00000000..3e08924c
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java
@@ -0,0 +1,246 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.WindowInsetsAnimation;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.NestedScrollingParent3;
+import androidx.core.view.NestedScrollingParentHelper;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import java.util.Arrays;
+
+import awais.instagrabber.customviews.helpers.SimpleImeAnimationController;
+import awais.instagrabber.utils.ViewUtils;
+
+import static androidx.core.view.ViewCompat.TYPE_TOUCH;
+
+public final class InsetsAnimationLinearLayout extends LinearLayout implements NestedScrollingParent3 {
+ private final NestedScrollingParentHelper nestedScrollingParentHelper = new NestedScrollingParentHelper(this);
+ private final SimpleImeAnimationController imeAnimController = new SimpleImeAnimationController();
+ private final int[] tempIntArray2 = new int[2];
+ private final int[] startViewLocation = new int[2];
+
+ private View currentNestedScrollingChild;
+ private int dropNextY;
+ private boolean scrollImeOffScreenWhenVisible = true;
+ private boolean scrollImeOnScreenWhenNotVisible = true;
+ private boolean scrollImeOffScreenWhenVisibleOnFling = false;
+ private boolean scrollImeOnScreenWhenNotVisibleOnFling = false;
+
+ public InsetsAnimationLinearLayout(final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InsetsAnimationLinearLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public final boolean getScrollImeOffScreenWhenVisible() {
+ return scrollImeOffScreenWhenVisible;
+ }
+
+ public final void setScrollImeOffScreenWhenVisible(boolean scrollImeOffScreenWhenVisible) {
+ this.scrollImeOffScreenWhenVisible = scrollImeOffScreenWhenVisible;
+ }
+
+ public final boolean getScrollImeOnScreenWhenNotVisible() {
+ return scrollImeOnScreenWhenNotVisible;
+ }
+
+ public final void setScrollImeOnScreenWhenNotVisible(boolean scrollImeOnScreenWhenNotVisible) {
+ this.scrollImeOnScreenWhenNotVisible = scrollImeOnScreenWhenNotVisible;
+ }
+
+ public boolean getScrollImeOffScreenWhenVisibleOnFling() {
+ return scrollImeOffScreenWhenVisibleOnFling;
+ }
+
+ public void setScrollImeOffScreenWhenVisibleOnFling(final boolean scrollImeOffScreenWhenVisibleOnFling) {
+ this.scrollImeOffScreenWhenVisibleOnFling = scrollImeOffScreenWhenVisibleOnFling;
+ }
+
+ public boolean getScrollImeOnScreenWhenNotVisibleOnFling() {
+ return scrollImeOnScreenWhenNotVisibleOnFling;
+ }
+
+ public void setScrollImeOnScreenWhenNotVisibleOnFling(final boolean scrollImeOnScreenWhenNotVisibleOnFling) {
+ this.scrollImeOnScreenWhenNotVisibleOnFling = scrollImeOnScreenWhenNotVisibleOnFling;
+ }
+
+ public SimpleImeAnimationController getImeAnimController() {
+ return imeAnimController;
+ }
+
+ @Override
+ public boolean onStartNestedScroll(@NonNull final View child,
+ @NonNull final View target,
+ final int axes,
+ final int type) {
+ return (axes & SCROLL_AXIS_VERTICAL) != 0 && type == TYPE_TOUCH;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(@NonNull final View child,
+ @NonNull final View target,
+ final int axes,
+ final int type) {
+ nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type);
+ currentNestedScrollingChild = child;
+ }
+
+ @Override
+ public void onNestedPreScroll(@NonNull final View target,
+ final int dx,
+ final int dy,
+ @NonNull final int[] consumed,
+ final int type) {
+ if (imeAnimController.isInsetAnimationRequestPending()) {
+ consumed[0] = dx;
+ consumed[1] = dy;
+ } else {
+ int deltaY = dy;
+ if (dropNextY != 0) {
+ consumed[1] = dropNextY;
+ deltaY = dy - dropNextY;
+ dropNextY = 0;
+ }
+
+ if (deltaY < 0) {
+ if (imeAnimController.isInsetAnimationInProgress()) {
+ consumed[1] -= imeAnimController.insetBy(-deltaY);
+ } else if (scrollImeOffScreenWhenVisible && !imeAnimController.isInsetAnimationRequestPending()) {
+ WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this);
+ if (rootWindowInsets != null) {
+ if (rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) {
+ startControlRequest();
+ consumed[1] = deltaY;
+ }
+ }
+ }
+ }
+
+ }
+ }
+
+ @Override
+ public void onNestedScroll(@NonNull final View target,
+ final int dxConsumed,
+ final int dyConsumed,
+ final int dxUnconsumed,
+ final int dyUnconsumed,
+ final int type,
+ @NonNull final int[] consumed) {
+ if (dyUnconsumed > 0) {
+ if (imeAnimController.isInsetAnimationInProgress()) {
+ consumed[1] = -imeAnimController.insetBy(-dyUnconsumed);
+ } else if (scrollImeOnScreenWhenNotVisible && !imeAnimController.isInsetAnimationRequestPending()) {
+ WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this);
+ if (rootWindowInsets != null) {
+ if (!rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) {
+ startControlRequest();
+ consumed[1] = dyUnconsumed;
+ }
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public boolean onNestedFling(@NonNull final View target,
+ final float velocityX,
+ final float velocityY,
+ final boolean consumed) {
+ if (imeAnimController.isInsetAnimationInProgress()) {
+ imeAnimController.animateToFinish(velocityY);
+ return true;
+ } else {
+ boolean imeVisible = false;
+ final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this);
+ if (rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) {
+ imeVisible = true;
+ }
+ if (velocityY > 0 && scrollImeOnScreenWhenNotVisibleOnFling && !imeVisible) {
+ imeAnimController.startAndFling(this, velocityY);
+ return true;
+ } else if (velocityY < 0 && scrollImeOffScreenWhenVisibleOnFling && imeVisible) {
+ imeAnimController.startAndFling(this, velocityY);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public void onStopNestedScroll(@NonNull final View target, final int type) {
+ nestedScrollingParentHelper.onStopNestedScroll(target, type);
+ if (imeAnimController.isInsetAnimationInProgress() && !imeAnimController.isInsetAnimationFinishing()) {
+ imeAnimController.animateToFinish(null);
+ }
+ reset();
+ }
+
+ @Override
+ public void dispatchWindowInsetsAnimationPrepare(@NonNull final WindowInsetsAnimation animation) {
+ super.dispatchWindowInsetsAnimationPrepare(animation);
+ ViewUtils.suppressLayoutCompat(this, false);
+ }
+
+ private void startControlRequest() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ return;
+ }
+ ViewUtils.suppressLayoutCompat(this, true);
+ if (currentNestedScrollingChild != null) {
+ currentNestedScrollingChild.getLocationInWindow(startViewLocation);
+ }
+ imeAnimController.startControlRequest(this, windowInsetsAnimationControllerCompat -> onControllerReady());
+ }
+
+ private void onControllerReady() {
+ if (currentNestedScrollingChild != null) {
+ imeAnimController.insetBy(0);
+ int[] location = tempIntArray2;
+ currentNestedScrollingChild.getLocationInWindow(location);
+ dropNextY = location[1] - startViewLocation[1];
+ }
+
+ }
+
+ private void reset() {
+ dropNextY = 0;
+ Arrays.fill(startViewLocation, 0);
+ ViewUtils.suppressLayoutCompat(this, false);
+ }
+
+ @Override
+ public void onNestedScrollAccepted(@NonNull final View child,
+ @NonNull final View target,
+ final int axes) {
+ onNestedScrollAccepted(child, target, axes, TYPE_TOUCH);
+ }
+
+ @Override
+ public void onNestedScroll(@NonNull final View target,
+ final int dxConsumed,
+ final int dyConsumed,
+ final int dxUnconsumed,
+ final int dyUnconsumed,
+ final int type) {
+ onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, tempIntArray2);
+ }
+
+ @Override
+ public void onStopNestedScroll(@NonNull final View target) {
+ onStopNestedScroll(target, TYPE_TOUCH);
+ }
+}
+
diff --git a/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java
new file mode 100644
index 00000000..13a93e43
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java
@@ -0,0 +1,33 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.WindowInsets;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+
+public class InsetsNotifyingCoordinatorLayout extends CoordinatorLayout {
+
+ public InsetsNotifyingCoordinatorLayout(@NonNull final Context context) {
+ super(context);
+ }
+
+ public InsetsNotifyingCoordinatorLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InsetsNotifyingCoordinatorLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ int childCount = getChildCount();
+ for (int index = 0; index < childCount; index++) {
+ getChildAt(index).dispatchApplyWindowInsets(insets);
+ }
+ return insets;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java
new file mode 100644
index 00000000..b2faa4e1
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java
@@ -0,0 +1,35 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.WindowInsets;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+public class InsetsNotifyingLinearLayout extends LinearLayout {
+ public InsetsNotifyingLinearLayout(final Context context) {
+ super(context);
+ }
+
+ public InsetsNotifyingLinearLayout(final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InsetsNotifyingLinearLayout(final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public InsetsNotifyingLinearLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ int childCount = getChildCount();
+ for (int index = 0; index < childCount; index++) {
+ getChildAt(index).dispatchApplyWindowInsets(insets);
+ }
+ return insets;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/NavHostFragmentWithDefaultAnimations.java b/app/src/main/java/awais/instagrabber/customviews/NavHostFragmentWithDefaultAnimations.java
new file mode 100644
index 00000000..11621a6c
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/NavHostFragmentWithDefaultAnimations.java
@@ -0,0 +1,60 @@
+package awais.instagrabber.customviews;
+
+import android.os.Bundle;
+
+import androidx.annotation.NavigationRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.navigation.NavController;
+import androidx.navigation.Navigator;
+import androidx.navigation.fragment.FragmentNavigator;
+import androidx.navigation.fragment.NavHostFragment;
+
+public class NavHostFragmentWithDefaultAnimations extends NavHostFragment {
+ private static final String KEY_GRAPH_ID = "android-support-nav:fragment:graphId";
+ private static final String KEY_START_DESTINATION_ARGS =
+ "android-support-nav:fragment:startDestinationArgs";
+ private static final String KEY_NAV_CONTROLLER_STATE =
+ "android-support-nav:fragment:navControllerState";
+ private static final String KEY_DEFAULT_NAV_HOST = "android-support-nav:fragment:defaultHost";
+
+ @NonNull
+ public static NavHostFragment create(@NavigationRes int graphResId) {
+ return create(graphResId, null);
+ }
+
+ @NonNull
+ public static NavHostFragment create(@NavigationRes int graphResId,
+ @Nullable Bundle startDestinationArgs) {
+ Bundle b = null;
+ if (graphResId != 0) {
+ b = new Bundle();
+ b.putInt(KEY_GRAPH_ID, graphResId);
+ }
+ if (startDestinationArgs != null) {
+ if (b == null) {
+ b = new Bundle();
+ }
+ b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
+ }
+
+ final NavHostFragmentWithDefaultAnimations result = new NavHostFragmentWithDefaultAnimations();
+ if (b != null) {
+ result.setArguments(b);
+ }
+ return result;
+ }
+
+ @NonNull
+ @Override
+ protected Navigator extends FragmentNavigator.Destination> createFragmentNavigator() {
+ return new FragmentNavigatorWithDefaultAnimations(requireContext(), getChildFragmentManager(), getId());
+ }
+
+ @Override
+ protected void onCreateNavController(@NonNull final NavController navController) {
+ super.onCreateNavController(navController);
+ navController.getNavigatorProvider()
+ .addNavigator(new FragmentNavigatorWithDefaultAnimations(requireContext(), getChildFragmentManager(), getId()));
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
index b2456e0a..ace25beb 100644
--- a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
+++ b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
@@ -3,6 +3,8 @@ package awais.instagrabber.customviews;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -25,15 +27,16 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.function.Function;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration;
import awais.instagrabber.customviews.helpers.PostFetcher;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge;
+import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.repositories.responses.Media;
-import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.KeywordsFilterUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
@@ -60,14 +63,17 @@ public class PostsRecyclerView extends RecyclerView {
private FeedAdapterV2.FeedItemCallback feedItemCallback;
private boolean shouldScrollToTop;
private FeedAdapterV2.SelectionModeCallback selectionModeCallback;
+ private Function headerViewCreator;
+ private Function headerBinder;
+ private boolean refresh = true;
private final List fetchStatusChangeListeners = new ArrayList<>();
private final FetchListener> fetchListener = new FetchListener>() {
@Override
public void onResult(final List result) {
- final int currentPage = lazyLoader.getCurrentPage();
- if (currentPage == 0) {
+ if (refresh) {
+ refresh = false;
mediaViewModel.getList().postValue(result);
shouldScrollToTop = true;
dispatchFetchStatus();
@@ -75,8 +81,8 @@ public class PostsRecyclerView extends RecyclerView {
}
final List models = mediaViewModel.getList().getValue();
final List modelsCopy = models == null ? new ArrayList<>() : new ArrayList<>(models);
- if (settingsHelper.getBoolean(Constants.TOGGLE_KEYWORD_FILTER)) {
- final ArrayList items = new ArrayList<>(settingsHelper.getStringSet(Constants.KEYWORD_FILTERS));
+ if (settingsHelper.getBoolean(PreferenceKeys.TOGGLE_KEYWORD_FILTER)) {
+ final ArrayList items = new ArrayList<>(settingsHelper.getStringSet(PreferenceKeys.KEYWORD_FILTERS));
modelsCopy.addAll(new KeywordsFilterUtils(items).filter(result));
} else {
modelsCopy.addAll(result);
@@ -192,22 +198,25 @@ public class PostsRecyclerView extends RecyclerView {
}
private void initSelf() {
- mediaViewModel = new ViewModelProvider(viewModelStoreOwner).get(MediaViewModel.class);
- mediaViewModel.getList().observe(lifeCycleOwner, list -> {
- if (list.size() <= 0) return;
- feedAdapter.submitList(list, () -> {
- // postDelayed(this::fetchMoreIfPossible, 1000);
- if (!shouldScrollToTop) return;
- smoothScrollToPosition(0);
- shouldScrollToTop = false;
- });
- });
+ try {
+ mediaViewModel = new ViewModelProvider(viewModelStoreOwner).get(MediaViewModel.class);
+ } catch (Exception e) {
+ Log.e(TAG, "initSelf: ", e);
+ }
+ if (mediaViewModel == null) return;
+ mediaViewModel.getList().observe(lifeCycleOwner, list -> feedAdapter.submitList(list, () -> {
+ // postDelayed(this::fetchMoreIfPossible, 1000);
+ if (!shouldScrollToTop) return;
+ shouldScrollToTop = false;
+ post(() -> smoothScrollToPosition(0));
+ }));
postFetcher = new PostFetcher(postFetchService, fetchListener);
if (layoutPreferences.getHasGap()) {
addItemDecoration(gridSpacingItemDecoration);
}
setHasFixedSize(true);
setNestedScrollingEnabled(true);
+ setItemAnimator(null);
lazyLoader = new RecyclerLazyLoaderAtEdge(layoutManager, (page) -> {
if (postFetcher.hasMore()) {
postFetcher.fetch();
@@ -311,11 +320,12 @@ public class PostsRecyclerView extends RecyclerView {
}
public void refresh() {
+ refresh = true;
if (lazyLoader != null) {
lazyLoader.resetState();
}
if (postFetcher != null) {
- mediaViewModel.getList().postValue(Collections.emptyList());
+ // mediaViewModel.getList().postValue(Collections.emptyList());
postFetcher.reset();
postFetcher.fetch();
}
diff --git a/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java b/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java
index 2f8409de..491127a6 100644
--- a/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java
+++ b/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java
@@ -70,6 +70,9 @@ public final class ProfilePicView extends CircularImageView {
case SMALL:
dimenRes = R.dimen.profile_pic_size_small;
break;
+ case SMALLER:
+ dimenRes = R.dimen.profile_pic_size_smaller;
+ break;
case TINY:
dimenRes = R.dimen.profile_pic_size_tiny;
break;
@@ -113,7 +116,8 @@ public final class ProfilePicView extends CircularImageView {
TINY(0),
SMALL(1),
REGULAR(2),
- LARGE(3);
+ LARGE(3),
+ SMALLER(4);
private final int value;
private static final Map map = new HashMap<>();
diff --git a/app/src/main/java/awais/instagrabber/customviews/RamboTextViewV2.java b/app/src/main/java/awais/instagrabber/customviews/RamboTextViewV2.java
index 14f09798..a96766af 100644
--- a/app/src/main/java/awais/instagrabber/customviews/RamboTextViewV2.java
+++ b/app/src/main/java/awais/instagrabber/customviews/RamboTextViewV2.java
@@ -1,10 +1,12 @@
package awais.instagrabber.customviews;
import android.content.Context;
+import android.text.InputFilter;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.emoji.widget.EmojiTextViewHelper;
import java.util.ArrayList;
import java.util.List;
@@ -23,6 +25,8 @@ public class RamboTextViewV2 extends AutoLinkTextView {
private final List onURLClickListeners = new ArrayList<>();
private final List onEmailClickListeners = new ArrayList<>();
+ private EmojiTextViewHelper emojiTextViewHelper;
+
public RamboTextViewV2(@NonNull final Context context,
@Nullable final AttributeSet attrs) {
super(context, attrs);
@@ -30,6 +34,7 @@ public class RamboTextViewV2 extends AutoLinkTextView {
}
private void init() {
+ getEmojiTextViewHelper().updateTransformationMethod();
addAutoLinkMode(MODE_HASHTAG.INSTANCE, MODE_MENTION.INSTANCE, MODE_EMAIL.INSTANCE, MODE_URL.INSTANCE);
onAutoLinkClick(autoLinkItem -> {
final Mode mode = autoLinkItem.getMode();
@@ -57,6 +62,26 @@ public class RamboTextViewV2 extends AutoLinkTextView {
}
}
});
+ onAutoLinkLongClick(autoLinkItem -> {});
+ }
+
+ @Override
+ public void setFilters(InputFilter[] filters) {
+ super.setFilters(getEmojiTextViewHelper().getFilters(filters));
+ }
+
+ @Override
+ public void setAllCaps(boolean allCaps) {
+ super.setAllCaps(allCaps);
+ getEmojiTextViewHelper().setAllCaps(allCaps);
+ }
+
+
+ private EmojiTextViewHelper getEmojiTextViewHelper() {
+ if (emojiTextViewHelper == null) {
+ emojiTextViewHelper = new EmojiTextViewHelper(this);
+ }
+ return emojiTextViewHelper;
}
public void addOnMentionClickListener(final OnMentionClickListener onMentionClickListener) {
diff --git a/app/src/main/java/awais/instagrabber/customviews/TextViewDrawableSize.java b/app/src/main/java/awais/instagrabber/customviews/TextViewDrawableSize.java
new file mode 100644
index 00000000..d4c96b8c
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/TextViewDrawableSize.java
@@ -0,0 +1,95 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.emoji.widget.EmojiAppCompatTextView;
+
+import awais.instagrabber.R;
+
+/**
+ * https://stackoverflow.com/a/31916731
+ */
+public class TextViewDrawableSize extends EmojiAppCompatTextView {
+
+ private int mDrawableWidth;
+ private int mDrawableHeight;
+ private boolean calledFromInit = false;
+
+ public TextViewDrawableSize(final Context context) {
+ this(context, null);
+ }
+
+ public TextViewDrawableSize(final Context context, final AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TextViewDrawableSize(final Context context, final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr);
+ }
+
+ private void init(@NonNull final Context context, final AttributeSet attrs, final int defStyleAttr) {
+ final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextViewDrawableSize, defStyleAttr, 0);
+
+ try {
+ mDrawableWidth = array.getDimensionPixelSize(R.styleable.TextViewDrawableSize_compoundDrawableWidth, -1);
+ mDrawableHeight = array.getDimensionPixelSize(R.styleable.TextViewDrawableSize_compoundDrawableHeight, -1);
+ } finally {
+ array.recycle();
+ }
+
+ if (mDrawableWidth > 0 || mDrawableHeight > 0) {
+ initCompoundDrawableSize();
+ }
+ }
+
+ private void initCompoundDrawableSize() {
+ final Drawable[] drawables = getCompoundDrawablesRelative();
+ for (Drawable drawable : drawables) {
+ if (drawable == null) {
+ continue;
+ }
+
+ final Rect realBounds = drawable.getBounds();
+ float scaleFactor = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth();
+
+ float drawableWidth = drawable.getIntrinsicWidth();
+ float drawableHeight = drawable.getIntrinsicHeight();
+
+ if (mDrawableWidth > 0) {
+ // save scale factor of image
+ if (drawableWidth > mDrawableWidth) {
+ drawableWidth = mDrawableWidth;
+ drawableHeight = drawableWidth * scaleFactor;
+ }
+ }
+ if (mDrawableHeight > 0) {
+ // save scale factor of image
+ if (drawableHeight > mDrawableHeight) {
+ drawableHeight = mDrawableHeight;
+ drawableWidth = drawableHeight / scaleFactor;
+ }
+ }
+
+ realBounds.right = realBounds.left + Math.round(drawableWidth);
+ realBounds.bottom = realBounds.top + Math.round(drawableHeight);
+
+ drawable.setBounds(realBounds);
+ }
+ setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3]);
+ }
+
+ public void setCompoundDrawablesRelativeWithSize(@Nullable final Drawable start,
+ @Nullable final Drawable top,
+ @Nullable final Drawable end,
+ @Nullable final Drawable bottom) {
+ setCompoundDrawablesRelative(start, top, end, bottom);
+ initCompoundDrawableSize();
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/Tooltip.java b/app/src/main/java/awais/instagrabber/customviews/Tooltip.java
index 91a07e42..709f23b4 100644
--- a/app/src/main/java/awais/instagrabber/customviews/Tooltip.java
+++ b/app/src/main/java/awais/instagrabber/customviews/Tooltip.java
@@ -9,12 +9,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
+import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatTextView;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.utils.ViewUtils;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
public class Tooltip extends AppCompatTextView {
@@ -22,7 +24,7 @@ public class Tooltip extends AppCompatTextView {
private ViewPropertyAnimator animator;
private boolean showing;
- private final AppExecutors appExecutors;
+ private final AppExecutors appExecutors = AppExecutors.INSTANCE;
private final Runnable dismissRunnable = () -> {
animator = animate().alpha(0).setListener(new AnimatorListenerAdapter() {
@Override
@@ -33,17 +35,15 @@ public class Tooltip extends AppCompatTextView {
animator.start();
};
- public Tooltip(Context context, ViewGroup parentView, int backgroundColor, int textColor) {
+ public Tooltip(@NonNull Context context, @NonNull ViewGroup parentView, int backgroundColor, int textColor) {
super(context);
setBackgroundDrawable(ViewUtils.createRoundRectDrawable(Utils.convertDpToPx(3), backgroundColor));
setTextColor(textColor);
setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
setPadding(Utils.convertDpToPx(8), Utils.convertDpToPx(7), Utils.convertDpToPx(8), Utils.convertDpToPx(7));
setGravity(Gravity.CENTER_VERTICAL);
- parentView.addView(this, ViewUtils.createFrame(
- ViewUtils.WRAP_CONTENT, ViewUtils.WRAP_CONTENT, Gravity.START | Gravity.TOP, 5, 0, 5, 3));
+ parentView.addView(this, ViewUtils.createFrame(WRAP_CONTENT, WRAP_CONTENT, Gravity.START | Gravity.TOP, 5, 0, 5, 3));
setVisibility(GONE);
- appExecutors = AppExecutors.getInstance();
}
@Override
@@ -87,8 +87,8 @@ public class Tooltip extends AppCompatTextView {
updateTooltipPosition();
showing = true;
- appExecutors.mainThread().cancel(dismissRunnable);
- appExecutors.mainThread().execute(dismissRunnable, 2000);
+ appExecutors.getMainThread().cancel(dismissRunnable);
+ appExecutors.getMainThread().execute(dismissRunnable, 2000);
if (animator != null) {
animator.setListener(null);
animator.cancel();
@@ -110,7 +110,7 @@ public class Tooltip extends AppCompatTextView {
animator = null;
}
- appExecutors.mainThread().cancel(dismissRunnable);
+ appExecutors.getMainThread().cancel(dismissRunnable);
dismissRunnable.run();
}
showing = false;
diff --git a/app/src/main/java/awais/instagrabber/customviews/UsernameTextView.java b/app/src/main/java/awais/instagrabber/customviews/UsernameTextView.java
new file mode 100644
index 00000000..c2da60c3
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/UsernameTextView.java
@@ -0,0 +1,77 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.appcompat.widget.AppCompatTextView;
+
+import awais.instagrabber.R;
+import awais.instagrabber.utils.Utils;
+
+public class UsernameTextView extends AppCompatTextView {
+ private static final String TAG = UsernameTextView.class.getSimpleName();
+
+ private final int drawableSize = Utils.convertDpToPx(24);
+
+ private boolean verified;
+ private VerticalImageSpan verifiedSpan;
+
+ public UsernameTextView(@NonNull final Context context) {
+ this(context, null);
+ }
+
+ public UsernameTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public UsernameTextView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ try {
+ final Drawable verifiedDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.verified);
+ final Drawable drawable = verifiedDrawable.mutate();
+ drawable.setBounds(0, 0, drawableSize, drawableSize);
+ verifiedSpan = new VerticalImageSpan(drawable);
+ } catch (Exception e) {
+ Log.e(TAG, "init: ", e);
+ }
+ }
+
+ public void setUsername(final CharSequence username) {
+ setUsername(username, false);
+ }
+
+ public void setUsername(final CharSequence username, final boolean verified) {
+ this.verified = verified;
+ final SpannableStringBuilder sb = new SpannableStringBuilder(username);
+ if (verified) {
+ try {
+ if (verifiedSpan != null) {
+ sb.append(" ");
+ sb.setSpan(verifiedSpan, sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "bind: ", e);
+ }
+ }
+ super.setText(sb);
+ }
+
+ public boolean isVerified() {
+ return verified;
+ }
+
+ public void setVerified(final boolean verified) {
+ setUsername(getText(), verified);
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java
index 30b9bbca..dd75c15f 100644
--- a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java
+++ b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java
@@ -1,5 +1,7 @@
package awais.instagrabber.customviews;
+import com.google.android.exoplayer2.ui.StyledPlayerView;
+
public class VideoPlayerCallbackAdapter implements VideoPlayerViewHelper.VideoPlayerCallback {
@Override
public void onThumbnailLoaded() {}
@@ -18,4 +20,12 @@ public class VideoPlayerCallbackAdapter implements VideoPlayerViewHelper.VideoPl
@Override
public void onRelease() {}
+
+ @Override
+ public void onFullScreenModeChanged(final boolean isFullScreen, final StyledPlayerView playerView) {}
+
+ @Override
+ public boolean isInFullScreen() {
+ return false;
+ }
}
diff --git a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java
index b2cfc2f1..6ce21a77 100644
--- a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java
+++ b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java
@@ -1,123 +1,70 @@
package awais.instagrabber.customviews;
import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
import android.graphics.drawable.Animatable;
import android.net.Uri;
-import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
import androidx.annotation.NonNull;
-import androidx.appcompat.view.ContextThemeWrapper;
-import androidx.appcompat.widget.AppCompatTextView;
-import androidx.appcompat.widget.PopupMenu;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageButton;
import com.facebook.drawee.backends.pipeline.Fresco;
+import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder;
import com.facebook.drawee.controller.BaseControllerListener;
-import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem;
-import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
+import com.google.android.exoplayer2.ui.StyledPlayerControlView;
+import com.google.android.exoplayer2.ui.StyledPlayerView;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
-import com.google.android.material.slider.LabelFormatter;
-import com.google.android.material.slider.Slider;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import awais.instagrabber.R;
-import awais.instagrabber.databinding.LayoutExoCustomControlsBinding;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
-import awais.instagrabber.utils.TextUtils;
-
-import static com.google.android.exoplayer2.C.TIME_UNSET;
-import static com.google.android.exoplayer2.Player.STATE_ENDED;
-import static com.google.android.exoplayer2.Player.STATE_IDLE;
-import static com.google.android.exoplayer2.Player.STATE_READY;
+import awais.instagrabber.utils.Utils;
public class VideoPlayerViewHelper implements Player.EventListener {
- private static final String TAG = "VideoPlayerViewHelper";
- private static final long INITIAL_DELAY = 0;
- private static final long RECURRING_DELAY = 60;
+ private static final String TAG = VideoPlayerViewHelper.class.getSimpleName();
private final Context context;
- private final awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding binding;
+ private final LayoutVideoPlayerWithThumbnailBinding binding;
private final float initialVolume;
private final float thumbnailAspectRatio;
private final String thumbnailUrl;
private final boolean loadPlayerOnClick;
- private final awais.instagrabber.databinding.LayoutExoCustomControlsBinding controlsBinding;
private final VideoPlayerCallback videoPlayerCallback;
private final String videoUrl;
private final DefaultDataSourceFactory dataSourceFactory;
private SimpleExoPlayer player;
- private PopupMenu speedPopup;
- private PositionCheckRunnable positionChecker;
- private Handler positionUpdateHandler;
+ private AppCompatImageButton mute;
- private final Player.EventListener listener = new Player.EventListener() {
- @Override
- public void onPlaybackStateChanged(final int state) {
- switch (state) {
- case Player.STATE_BUFFERING:
- case STATE_IDLE:
- case STATE_ENDED:
- positionUpdateHandler.removeCallbacks(positionChecker);
- return;
- case STATE_READY:
- setupTimeline();
- positionUpdateHandler.postDelayed(positionChecker, INITIAL_DELAY);
- break;
- }
- }
-
- @Override
- public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason) {
- updatePlayPauseDrawable(playWhenReady);
- if (positionUpdateHandler == null || positionChecker == null) return;
- if (playWhenReady) {
- positionUpdateHandler.removeCallbacks(positionChecker);
- positionUpdateHandler.postDelayed(positionChecker, INITIAL_DELAY);
- }
- }
- };
private final AudioListener audioListener = new AudioListener() {
@Override
public void onVolumeChanged(final float volume) {
updateMuteIcon(volume);
}
};
- private final Slider.OnChangeListener onChangeListener = (slider, value, fromUser) -> {
- if (!fromUser) return;
- long actualValue = (long) value;
- if (actualValue < 0) {
- actualValue = 0;
- } else if (actualValue > player.getDuration()) {
- actualValue = player.getDuration();
- }
- player.seekTo(actualValue);
- };
- private final View.OnClickListener onClickListener = v -> player.setPlayWhenReady(!player.getPlayWhenReady());
- private final LabelFormatter labelFormatter = value -> TextUtils.millisToTimeString((long) value);
private final View.OnClickListener muteOnClickListener = v -> toggleMute();
- private final View.OnClickListener rewOnClickListener = v -> {
- final long positionMs = player.getCurrentPosition() - 5000;
- player.seekTo(positionMs < 0 ? 0 : positionMs);
- };
- private final View.OnClickListener ffOnClickListener = v -> {
- long positionMs = player.getCurrentPosition() + 5000;
- long duration = player.getDuration();
- if (duration == TIME_UNSET) {
- duration = 0;
- }
- player.seekTo(Math.min(positionMs, duration));
- };
- private final View.OnClickListener showMenu = this::showMenu;
+ private Object layoutManager;
public VideoPlayerViewHelper(@NonNull final Context context,
@NonNull final LayoutVideoPlayerWithThumbnailBinding binding,
@@ -126,7 +73,6 @@ public class VideoPlayerViewHelper implements Player.EventListener {
final float thumbnailAspectRatio,
final String thumbnailUrl,
final boolean loadPlayerOnClick,
- final LayoutExoCustomControlsBinding controlsBinding,
final VideoPlayerCallback videoPlayerCallback) {
this.context = context;
this.binding = binding;
@@ -134,7 +80,6 @@ public class VideoPlayerViewHelper implements Player.EventListener {
this.thumbnailAspectRatio = thumbnailAspectRatio;
this.thumbnailUrl = thumbnailUrl;
this.loadPlayerOnClick = loadPlayerOnClick;
- this.controlsBinding = controlsBinding;
this.videoPlayerCallback = videoPlayerCallback;
this.videoUrl = videoUrl;
this.dataSourceFactory = new DefaultDataSourceFactory(binding.getRoot().getContext(), "instagram");
@@ -151,40 +96,43 @@ public class VideoPlayerViewHelper implements Player.EventListener {
}
});
setThumbnail();
- setupControls();
}
private void setThumbnail() {
binding.thumbnail.setAspectRatio(thumbnailAspectRatio);
- final ImageRequest thumbnailRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl))
- .build();
- final DraweeController controller = Fresco.newDraweeControllerBuilder()
- .setControllerListener(new BaseControllerListener() {
- @Override
- public void onFailure(final String id, final Throwable throwable) {
- if (videoPlayerCallback != null) {
- videoPlayerCallback.onThumbnailLoaded();
- }
- }
+ ImageRequest thumbnailRequest = null;
+ if (thumbnailUrl != null) {
+ thumbnailRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl)).build();
+ }
+ final PipelineDraweeControllerBuilder builder = Fresco
+ .newDraweeControllerBuilder()
+ .setControllerListener(new BaseControllerListener() {
+ @Override
+ public void onFailure(final String id, final Throwable throwable) {
+ if (videoPlayerCallback != null) {
+ videoPlayerCallback.onThumbnailLoaded();
+ }
+ }
- @Override
- public void onFinalImageSet(final String id,
- final ImageInfo imageInfo,
- final Animatable animatable) {
- if (videoPlayerCallback != null) {
- videoPlayerCallback.onThumbnailLoaded();
- }
- }
- })
- .setImageRequest(thumbnailRequest)
- .build();
- binding.thumbnail.setController(controller);
+ @Override
+ public void onFinalImageSet(final String id,
+ final ImageInfo imageInfo,
+ final Animatable animatable) {
+ if (videoPlayerCallback != null) {
+ videoPlayerCallback.onThumbnailLoaded();
+ }
+ }
+ });
+ if (thumbnailRequest != null) {
+ builder.setImageRequest(thumbnailRequest);
+ }
+ binding.thumbnail.setController(builder.build());
}
private void loadPlayer() {
if (videoUrl == null) return;
- if (binding.root.getDisplayedChild() == 0) {
- binding.root.showNext();
+ if (binding.getRoot().getDisplayedChild() == 0) {
+ binding.getRoot().showNext();
}
if (videoPlayerCallback != null) {
videoPlayerCallback.onPlayerViewLoaded();
@@ -193,15 +141,15 @@ public class VideoPlayerViewHelper implements Player.EventListener {
if (player != null) {
player.release();
}
+ final ViewGroup.LayoutParams playerViewLayoutParams = binding.playerView.getLayoutParams();
+ if (playerViewLayoutParams.height > Utils.displayMetrics.heightPixels * 0.8) {
+ playerViewLayoutParams.height = (int) (Utils.displayMetrics.heightPixels * 0.8);
+ }
player = new SimpleExoPlayer.Builder(context)
.setLooper(Looper.getMainLooper())
.build();
- positionUpdateHandler = new Handler();
- positionChecker = new PositionCheckRunnable(positionUpdateHandler,
- player,
- controlsBinding.timeline,
- controlsBinding.fromTime);
player.addListener(this);
+ player.addAudioListener(audioListener);
player.setVolume(initialVolume);
player.setPlayWhenReady(true);
player.setRepeatMode(Player.REPEAT_MODE_ALL);
@@ -209,123 +157,116 @@ public class VideoPlayerViewHelper implements Player.EventListener {
final MediaItem mediaItem = MediaItem.fromUri(videoUrl);
final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(mediaItem);
player.setMediaSource(mediaSource);
- setupControls();
player.prepare();
binding.playerView.setPlayer(player);
+ binding.playerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
+ binding.playerView.setShowNextButton(false);
+ binding.playerView.setShowPreviousButton(false);
+ binding.playerView.setControllerOnFullScreenModeChangedListener(isFullScreen -> {
+ if (videoPlayerCallback == null) return;
+ videoPlayerCallback.onFullScreenModeChanged(isFullScreen, binding.playerView);
+ });
+ setupControllerView();
}
- private void setupControls() {
- if (controlsBinding == null) return;
- binding.playerView.setUseController(false);
- if (player == null) {
- enableControls(false);
- // controlsBinding.playPause.setEnabled(true);
- // controlsBinding.playPause.setOnClickListener(new NoPlayerPlayPauseClickListener(binding.thumbnailParent));
+ private void setupControllerView() {
+ try {
+ final StyledPlayerControlView controllerView = getStyledPlayerControlView();
+ if (controllerView == null) return;
+ layoutManager = setControlViewLayoutManager(controllerView);
+ if (videoPlayerCallback != null && videoPlayerCallback.isInFullScreen()) {
+ setControllerViewToFullScreenMode(controllerView);
+ }
+ final ViewGroup exoBasicControls = controllerView.findViewById(R.id.exo_basic_controls);
+ if (exoBasicControls == null) return;
+ mute = new AppCompatImageButton(context);
+ final Resources resources = context.getResources();
+ if (resources == null) return;
+ final int width = resources.getDimensionPixelSize(R.dimen.exo_small_icon_width);
+ final int height = resources.getDimensionPixelSize(R.dimen.exo_small_icon_height);
+ final int margin = resources.getDimensionPixelSize(R.dimen.exo_small_icon_horizontal_margin);
+ final int paddingHorizontal = resources.getDimensionPixelSize(R.dimen.exo_small_icon_padding_horizontal);
+ final int paddingVertical = resources.getDimensionPixelSize(R.dimen.exo_small_icon_padding_vertical);
+ final ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(width, height);
+ layoutParams.setMargins(margin, 0, margin, 0);
+ mute.setLayoutParams(layoutParams);
+ mute.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical);
+ mute.setScaleType(ImageView.ScaleType.FIT_XY);
+ mute.setBackgroundResource(Utils.getAttrResId(context, android.R.attr.selectableItemBackground));
+ mute.setImageTintList(ColorStateList.valueOf(resources.getColor(R.color.white)));
+ updateMuteIcon(player.getVolume());
+ exoBasicControls.addView(mute, 0);
+ mute.setOnClickListener(muteOnClickListener);
+ } catch (Exception e) {
+ Log.e(TAG, "loadPlayer: ", e);
+ }
+ }
+
+ @Nullable
+ private Object setControlViewLayoutManager(@NonNull final StyledPlayerControlView controllerView)
+ throws NoSuchFieldException, IllegalAccessException {
+ final Field controlViewLayoutManagerField = controllerView.getClass().getDeclaredField("controlViewLayoutManager");
+ controlViewLayoutManagerField.setAccessible(true);
+ return controlViewLayoutManagerField.get(controllerView);
+ }
+
+ private void setControllerViewToFullScreenMode(@NonNull final StyledPlayerControlView controllerView)
+ throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
+ // Exoplayer doesn't expose the fullscreen state, so using reflection
+ final Field fullScreenButtonField = controllerView.getClass().getDeclaredField("fullScreenButton");
+ fullScreenButtonField.setAccessible(true);
+ final ImageView fullScreenButton = (ImageView) fullScreenButtonField.get(controllerView);
+ final Field isFullScreen = controllerView.getClass().getDeclaredField("isFullScreen");
+ isFullScreen.setAccessible(true);
+ isFullScreen.set(controllerView, true);
+ final Method updateFullScreenButtonForState = controllerView
+ .getClass()
+ .getDeclaredMethod("updateFullScreenButtonForState", ImageView.class, boolean.class);
+ updateFullScreenButtonForState.setAccessible(true);
+ updateFullScreenButtonForState.invoke(controllerView, fullScreenButton, true);
+
+ }
+
+ @Nullable
+ private StyledPlayerControlView getStyledPlayerControlView() throws NoSuchFieldException, IllegalAccessException {
+ final Field controller = binding.playerView.getClass().getDeclaredField("controller");
+ controller.setAccessible(true);
+ return (StyledPlayerControlView) controller.get(binding.playerView);
+ }
+
+ @Override
+ public void onTracksChanged(@NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) {
+ if (trackGroups.isEmpty()) {
+ setHasAudio(false);
return;
}
- enableControls(true);
- updatePlayPauseDrawable(player.getPlayWhenReady());
- updateMuteIcon(player.getVolume());
- player.addListener(listener);
- player.addAudioListener(audioListener);
- controlsBinding.timeline.addOnChangeListener(onChangeListener);
- controlsBinding.timeline.setLabelFormatter(labelFormatter);
- controlsBinding.playPause.setOnClickListener(onClickListener);
- controlsBinding.mute.setOnClickListener(muteOnClickListener);
- controlsBinding.rewWithAmount.setOnClickListener(rewOnClickListener);
- controlsBinding.ffWithAmount.setOnClickListener(ffOnClickListener);
- controlsBinding.speed.setOnClickListener(showMenu);
- }
-
- private void setupTimeline() {
- final long duration = player.getDuration();
- controlsBinding.timeline.setEnabled(true);
- controlsBinding.timeline.setValueFrom(0);
- controlsBinding.timeline.setValueTo(duration);
- controlsBinding.fromTime.setText(TextUtils.millisToTimeString(0));
- controlsBinding.toTime.setText(TextUtils.millisToTimeString(duration));
- }
-
- private void enableControls(final boolean enable) {
- controlsBinding.speed.setEnabled(enable);
- controlsBinding.speed.setClickable(enable);
- controlsBinding.mute.setEnabled(enable);
- controlsBinding.mute.setClickable(enable);
- controlsBinding.ffWithAmount.setEnabled(enable);
- controlsBinding.ffWithAmount.setClickable(enable);
- controlsBinding.rewWithAmount.setEnabled(enable);
- controlsBinding.rewWithAmount.setClickable(enable);
- controlsBinding.fromTime.setEnabled(enable);
- controlsBinding.toTime.setEnabled(enable);
- controlsBinding.playPause.setEnabled(enable);
- controlsBinding.playPause.setClickable(enable);
- controlsBinding.timeline.setEnabled(enable);
- }
-
- public void showMenu(View anchor) {
- PopupMenu popup = getPopupMenu(anchor);
- popup.show();
- }
-
- @NonNull
- private PopupMenu getPopupMenu(final View anchor) {
- if (speedPopup != null) {
- return speedPopup;
- }
- final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(context, R.style.popupMenuStyle);
- // final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(context, R.style.Widget_MaterialComponents_PopupMenu_Exoplayer);
- speedPopup = new PopupMenu(themeWrapper, anchor);
- speedPopup.getMenuInflater().inflate(R.menu.speed_menu, speedPopup.getMenu());
- speedPopup.setOnMenuItemClickListener(item -> {
- float nextSpeed;
- int textResId;
- int itemId = item.getItemId();
- if (itemId == R.id.pt_two_five_x) {
- nextSpeed = 0.25f;
- textResId = R.string.pt_two_five_x;
- } else if (itemId == R.id.pt_five_x) {
- nextSpeed = 0.5f;
- textResId = R.string.pt_five_x;
- } else if (itemId == R.id.pt_seven_five_x) {
- nextSpeed = 0.75f;
- textResId = R.string.pt_seven_five_x;
- } else if (itemId == R.id.one_x) {
- nextSpeed = 1f;
- textResId = R.string.one_x;
- } else if (itemId == R.id.one_pt_two_five_x) {
- nextSpeed = 1.25f;
- textResId = R.string.one_pt_two_five_x;
- } else if (itemId == R.id.one_pt_five_x) {
- nextSpeed = 1.5f;
- textResId = R.string.one_pt_five_x;
- } else if (itemId == R.id.two_x) {
- nextSpeed = 2f;
- textResId = R.string.two_x;
- } else {
- nextSpeed = 1;
- textResId = R.string.one_x;
+ boolean hasAudio = false;
+ for (int i = 0; i < trackGroups.length; i++) {
+ for (int g = 0; g < trackGroups.get(i).length; g++) {
+ final String sampleMimeType = trackGroups.get(i).getFormat(g).sampleMimeType;
+ if (sampleMimeType != null && sampleMimeType.contains("audio")) {
+ hasAudio = true;
+ break;
+ }
}
- player.setPlaybackParameters(new PlaybackParameters(nextSpeed));
- controlsBinding.speed.setText(textResId);
- return true;
- });
- return speedPopup;
+ }
+ setHasAudio(hasAudio);
+ }
+
+ private void setHasAudio(final boolean hasAudio) {
+ if (mute == null) return;
+ mute.setEnabled(hasAudio);
+ mute.setAlpha(hasAudio ? 1f : 0.5f);
+ updateMuteIcon(hasAudio ? 1f : 0f);
}
private void updateMuteIcon(final float volume) {
+ if (mute == null) return;
if (volume == 0) {
- controlsBinding.mute.setIconResource(R.drawable.ic_volume_off_24_states);
+ mute.setImageResource(R.drawable.ic_volume_off_24);
return;
}
- controlsBinding.mute.setIconResource(R.drawable.ic_volume_up_24_states);
- }
-
- private void updatePlayPauseDrawable(final boolean playWhenReady) {
- if (playWhenReady) {
- controlsBinding.playPause.setIconResource(R.drawable.ic_pause_24);
- return;
- }
- controlsBinding.playPause.setIconResource(R.drawable.ic_play_states);
+ mute.setImageResource(R.drawable.ic_volume_up_24);
}
@Override
@@ -339,25 +280,24 @@ public class VideoPlayerViewHelper implements Player.EventListener {
}
@Override
- public void onPlayerError(final ExoPlaybackException error) {
+ public void onPlayerError(@NonNull final ExoPlaybackException error) {
Log.e(TAG, "onPlayerError", error);
}
- public float toggleMute() {
- if (player == null) return 0;
+ private void toggleMute() {
+ if (player == null) return;
+ if (layoutManager != null) {
+ try {
+ final Method resetHideCallbacks = layoutManager.getClass().getDeclaredMethod("resetHideCallbacks");
+ resetHideCallbacks.invoke(layoutManager);
+ } catch (Exception e) {
+ Log.e(TAG, "toggleMute: ", e);
+ }
+ }
final float vol = player.getVolume() == 0f ? 1f : 0f;
player.setVolume(vol);
- return vol;
}
- // public void togglePlayback() {
- // if (player == null) return;
- // final int playbackState = player.getPlaybackState();
- // if (playbackState == STATE_IDLE || playbackState == STATE_ENDED) return;
- // final boolean playWhenReady = player.getPlayWhenReady();
- // player.setPlayWhenReady(!playWhenReady);
- // }
-
public void releasePlayer() {
if (videoPlayerCallback != null) {
videoPlayerCallback.onRelease();
@@ -366,84 +306,12 @@ public class VideoPlayerViewHelper implements Player.EventListener {
player.release();
player = null;
}
- if (positionUpdateHandler != null) {
- if (positionChecker != null) {
- positionUpdateHandler.removeCallbacks(positionChecker);
- positionChecker = null;
- }
- positionUpdateHandler = null;
- }
}
public void pause() {
if (player != null) {
player.pause();
}
- if (positionUpdateHandler != null) {
- if (positionChecker != null) {
- positionUpdateHandler.removeCallbacks(positionChecker);
- }
- }
- }
-
- public void resetTimeline() {
- if (player == null) {
- enableControls(false);
- return;
- }
- setupTimeline();
- final long currentPosition = player.getCurrentPosition();
- controlsBinding.timeline.setValue(Math.min(currentPosition, player.getDuration()));
- setupControls();
- }
-
- public void removeCallbacks() {
- if (player != null) {
- player.removeListener(listener);
- player.removeAudioListener(audioListener);
- }
- controlsBinding.timeline.removeOnChangeListener(onChangeListener);
- controlsBinding.timeline.setLabelFormatter(null);
- controlsBinding.playPause.setOnClickListener(null);
- controlsBinding.mute.setOnClickListener(null);
- controlsBinding.rewWithAmount.setOnClickListener(null);
- controlsBinding.ffWithAmount.setOnClickListener(null);
- controlsBinding.speed.setOnClickListener(null);
- }
-
- private static class PositionCheckRunnable implements Runnable {
- private final Handler positionUpdateHandler;
- private final SimpleExoPlayer player;
- private final Slider timeline;
- private final AppCompatTextView fromTime;
-
- public PositionCheckRunnable(final Handler positionUpdateHandler,
- final SimpleExoPlayer simpleExoPlayer,
- final Slider slider,
- final AppCompatTextView fromTime) {
- this.positionUpdateHandler = positionUpdateHandler;
- this.player = simpleExoPlayer;
- this.timeline = slider;
- this.fromTime = fromTime;
- }
-
- @Override
- public void run() {
- if (positionUpdateHandler == null) return;
- positionUpdateHandler.removeCallbacks(this);
- if (player == null) return;
- final long currentPosition = player.getCurrentPosition();
- final long duration = player.getDuration();
- if (duration == TIME_UNSET) {
- timeline.setValueFrom(0);
- timeline.setValueTo(0);
- timeline.setEnabled(false);
- return;
- }
- timeline.setValue(Math.min(currentPosition, duration));
- fromTime.setText(TextUtils.millisToTimeString(currentPosition));
- positionUpdateHandler.postDelayed(this, RECURRING_DELAY);
- }
}
public interface VideoPlayerCallback {
@@ -458,5 +326,9 @@ public class VideoPlayerViewHelper implements Player.EventListener {
void onPause();
void onRelease();
+
+ void onFullScreenModeChanged(boolean isFullScreen, final StyledPlayerView playerView);
+
+ boolean isInFullScreen();
}
}
diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java
index 5a7d55ad..d8b7b8f9 100644
--- a/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java
+++ b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java
@@ -228,7 +228,7 @@ public class ZoomableDraweeView extends DraweeView
public void setZoomingEnabled(boolean zoomingEnabled) {
mZoomingEnabled = zoomingEnabled;
- mZoomableController.setEnabled(false);
+ mZoomableController.setEnabled(zoomingEnabled);
}
/**
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java
new file mode 100644
index 00000000..340afc5d
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java
@@ -0,0 +1,102 @@
+package awais.instagrabber.customviews.emoji;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetDialog;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import awais.instagrabber.R;
+import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration;
+import awais.instagrabber.utils.Utils;
+import awais.instagrabber.utils.emoji.EmojiParser;
+
+public class EmojiBottomSheetDialog extends BottomSheetDialogFragment {
+ public static final String TAG = EmojiBottomSheetDialog.class.getSimpleName();
+
+ private RecyclerView grid;
+ private EmojiPicker.OnEmojiClickListener callback;
+
+ @NonNull
+ public static EmojiBottomSheetDialog newInstance() {
+ // Bundle args = new Bundle();
+ // fragment.setArguments(args);
+ return new EmojiBottomSheetDialog();
+ }
+
+ @Override
+ public void onCreate(@Nullable final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setStyle(DialogFragment.STYLE_NORMAL, R.style.ThemeOverlay_Rounded_BottomSheetDialog);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
+ final Context context = getContext();
+ if (context == null) return null;
+ grid = new RecyclerView(context);
+ return grid;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
+ init();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ final Dialog dialog = getDialog();
+ if (dialog == null) return;
+ final BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialog;
+ final View bottomSheetInternal = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);
+ if (bottomSheetInternal == null) return;
+ bottomSheetInternal.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
+ bottomSheetInternal.requestLayout();
+ }
+
+ @Override
+ public void onAttach(@NonNull final Context context) {
+ super.onAttach(context);
+ final Fragment parentFragment = getParentFragment();
+ if (parentFragment instanceof EmojiPicker.OnEmojiClickListener) {
+ callback = (EmojiPicker.OnEmojiClickListener) parentFragment;
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ grid = null;
+ super.onDestroyView();
+ }
+
+ private void init() {
+ final Context context = getContext();
+ if (context == null) return;
+ final GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 9);
+ grid.setLayoutManager(gridLayoutManager);
+ grid.setHasFixedSize(true);
+ grid.setClipToPadding(false);
+ grid.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(8)));
+ final EmojiParser emojiParser = EmojiParser.Companion.getInstance(context);
+ final EmojiGridAdapter adapter = new EmojiGridAdapter(emojiParser, null, (view, emoji) -> {
+ if (callback != null) {
+ callback.onClick(view, emoji);
+ }
+ dismiss();
+ }, null);
+ grid.setAdapter(adapter);
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiCategoryPageViewHolder.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiCategoryPageViewHolder.java
index b875c5a3..8af0cb52 100644
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiCategoryPageViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiCategoryPageViewHolder.java
@@ -6,12 +6,14 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.customviews.emoji.EmojiPicker.OnEmojiClickListener;
+import awais.instagrabber.utils.emoji.EmojiParser;
public class EmojiCategoryPageViewHolder extends RecyclerView.ViewHolder {
// private static final String TAG = EmojiCategoryPageViewHolder.class.getSimpleName();
private final View rootView;
private final OnEmojiClickListener onEmojiClickListener;
+ private final EmojiParser emojiParser = EmojiParser.Companion.getInstance(itemView.getContext());
public EmojiCategoryPageViewHolder(@NonNull final View rootView,
@NonNull final RecyclerView itemView,
@@ -24,6 +26,7 @@ public class EmojiCategoryPageViewHolder extends RecyclerView.ViewHolder {
public void bind(final EmojiCategory emojiCategory) {
final RecyclerView emojiGrid = (RecyclerView) itemView;
final EmojiGridAdapter adapter = new EmojiGridAdapter(
+ emojiParser,
emojiCategory.getType(),
onEmojiClickListener,
(position, view, parent) -> {
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java
index 5aff4472..02e96364 100644
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java
@@ -43,18 +43,23 @@ public class EmojiGridAdapter extends RecyclerView.Adapter(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
- final EmojiParser emojiParser = EmojiParser.getInstance();
final Map categoryMap = emojiParser.getCategoryMap();
emojiVariantManager = EmojiVariantManager.getInstance();
- appExecutors = AppExecutors.getInstance();
+ appExecutors = AppExecutors.INSTANCE;
setHasStableIds(true);
+ if (emojiCategoryType == null) {
+ // show all if type is null
+ differ.submitList(ImmutableList.copyOf(emojiParser.getAllEmojis().values()));
+ return;
+ }
final EmojiCategory emojiCategory = categoryMap.get(emojiCategoryType);
if (emojiCategory == null) {
differ.submitList(Collections.emptyList());
@@ -76,13 +81,13 @@ public class EmojiGridAdapter extends RecyclerView.Adapter {
+ appExecutors.getTasksThread().execute(() -> {
final Optional first = emoji.getVariants()
.stream()
.filter(e -> e.getUnicode().equals(variant))
.findFirst();
if (!first.isPresent()) return;
- appExecutors.mainThread().execute(() -> holder.bind(position, first.get(), emoji));
+ appExecutors.getMainThread().execute(() -> holder.bind(position, first.get(), emoji));
});
return;
}
@@ -105,7 +110,7 @@ public class EmojiGridAdapter extends RecyclerView.Adapter {
- binding.image.setImageDrawable(emoji.getDrawable());
- final boolean hasVariants = !parent.getVariants().isEmpty();
- binding.indicator.setVisibility(hasVariants ? View.VISIBLE : View.GONE);
- if (onEmojiClickListener != null) {
- itemView.setOnClickListener(v -> onEmojiClickListener.onClick(v, emoji));
- }
- if (hasVariants && onEmojiLongClickListener != null) {
- itemView.setOnLongClickListener(v -> onEmojiLongClickListener.onLongClick(position, v, parent));
- }
- });
+ // itemView.post(() -> {
+ binding.image.setImageDrawable(emoji.getDrawable());
+ final boolean hasVariants = !parent.getVariants().isEmpty();
+ binding.indicator.setVisibility(hasVariants ? View.VISIBLE : View.GONE);
+ if (onEmojiClickListener != null) {
+ itemView.setOnClickListener(v -> onEmojiClickListener.onClick(v, emoji));
+ }
+ if (hasVariants && onEmojiLongClickListener != null) {
+ itemView.setOnLongClickListener(v -> onEmojiLongClickListener.onLongClick(position, v, parent));
+ }
+ // });
}
}
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPicker.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPicker.java
index 1c79e77f..9621c246 100644
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPicker.java
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPicker.java
@@ -67,7 +67,9 @@ public class EmojiPicker extends LinearLayout {
viewPager2.setAdapter(new EmojiPickerPageAdapter(rootView, onEmojiClickListener));
viewPager2.setOffscreenPageLimit(1);
- final EmojiParser emojiParser = EmojiParser.getInstance();
+ final Context context = getContext();
+ if (context == null) return;
+ final EmojiParser emojiParser = EmojiParser.Companion.getInstance(context);
final List categories = emojiParser.getEmojiCategories();
new TabLayoutMediator(tabLayout, viewPager2, (tab, position) -> {
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPickerPageAdapter.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPickerPageAdapter.java
index d7b7ff03..7fe330b6 100644
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPickerPageAdapter.java
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPickerPageAdapter.java
@@ -37,13 +37,14 @@ public class EmojiPickerPageAdapter extends RecyclerView.Adapter differ;
- public EmojiPickerPageAdapter(final View rootView,
+ public EmojiPickerPageAdapter(@NonNull final View rootView,
final OnEmojiClickListener onEmojiClickListener) {
this.rootView = rootView;
this.onEmojiClickListener = onEmojiClickListener;
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
- differ.submitList(EmojiParser.getInstance().getEmojiCategories());
+ final EmojiParser emojiParser = EmojiParser.Companion.getInstance(rootView.getContext());
+ differ.submitList(emojiParser.getEmojiCategories());
setHasStableIds(true);
}
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java
deleted file mode 100644
index 76673590..00000000
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java
+++ /dev/null
@@ -1,163 +0,0 @@
-package awais.instagrabber.customviews.emoji;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.view.Gravity;
-import android.view.View;
-import android.view.WindowManager.LayoutParams;
-import android.widget.PopupWindow;
-
-import awais.instagrabber.R;
-import awais.instagrabber.customviews.emoji.EmojiPicker.OnBackspaceClickListener;
-import awais.instagrabber.customviews.emoji.EmojiPicker.OnEmojiClickListener;
-import awais.instagrabber.utils.Utils;
-
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-
-/**
- * https://stackoverflow.com/a/33897583/1436766
- */
-public class EmojiPopupWindow extends PopupWindow {
-
- private int keyBoardHeight = 0;
- private Boolean pendingOpen = false;
- private Boolean isOpened = false;
- private final View rootView;
- private final Context context;
- private final OnEmojiClickListener onEmojiClickListener;
- private final OnBackspaceClickListener onBackspaceClickListener;
-
- private OnSoftKeyboardOpenCloseListener onSoftKeyboardOpenCloseListener;
-
-
- /**
- * Constructor
- *
- * @param rootView The top most layout in your view hierarchy. The difference of this view and the screen height will be used to calculate the keyboard height.
- */
- public EmojiPopupWindow(final View rootView,
- final OnEmojiClickListener onEmojiClickListener,
- final OnBackspaceClickListener onBackspaceClickListener) {
- super(rootView.getContext());
- this.rootView = rootView;
- this.context = rootView.getContext();
- this.onEmojiClickListener = onEmojiClickListener;
- this.onBackspaceClickListener = onBackspaceClickListener;
- View customView = createCustomView();
- setContentView(customView);
- setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
- //default size
- setSize((int) context.getResources().getDimension(R.dimen.keyboard_height), MATCH_PARENT);
- }
-
- /**
- * Set the listener for the event of keyboard opening or closing.
- */
- public void setOnSoftKeyboardOpenCloseListener(OnSoftKeyboardOpenCloseListener listener) {
- this.onSoftKeyboardOpenCloseListener = listener;
- }
-
- /**
- * Use this function to show the emoji popup.
- * NOTE: Since, the soft keyboard sizes are variable on different android devices, the
- * library needs you to open the soft keyboard atleast once before calling this function.
- * If that is not possible see showAtBottomPending() function.
- */
- public void showAtBottom() {
- showAtLocation(rootView, Gravity.BOTTOM, 0, 0);
- }
-
- /**
- * Use this function when the soft keyboard has not been opened yet. This
- * will show the emoji popup after the keyboard is up next time.
- * Generally, you will be calling InputMethodManager.showSoftInput function after
- * calling this function.
- */
- public void showAtBottomPending() {
- if (isKeyBoardOpen())
- showAtBottom();
- else
- pendingOpen = true;
- }
-
- /**
- * @return Returns true if the soft keyboard is open, false otherwise.
- */
- public Boolean isKeyBoardOpen() {
- return isOpened;
- }
-
- /**
- * Dismiss the popup
- */
- @Override
- public void dismiss() {
- super.dismiss();
- }
-
- /**
- * Call this function to resize the emoji popup according to your soft keyboard size
- */
- public void setSizeForSoftKeyboard() {
- rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
- Rect r = new Rect();
- rootView.getWindowVisibleDisplayFrame(r);
-
- int screenHeight = getUsableScreenHeight();
- int heightDifference = screenHeight - (r.bottom - r.top);
- int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
- if (resourceId > 0) {
- heightDifference -= context.getResources()
- .getDimensionPixelSize(resourceId);
- }
- if (heightDifference > 100) {
- keyBoardHeight = heightDifference;
- setSize(MATCH_PARENT, keyBoardHeight);
- if (!isOpened) {
- if (onSoftKeyboardOpenCloseListener != null)
- onSoftKeyboardOpenCloseListener.onKeyboardOpen(keyBoardHeight);
- }
- isOpened = true;
- if (pendingOpen) {
- showAtBottom();
- pendingOpen = false;
- }
- } else {
- isOpened = false;
- if (onSoftKeyboardOpenCloseListener != null)
- onSoftKeyboardOpenCloseListener.onKeyboardClose();
- }
- });
- }
-
- private int getUsableScreenHeight() {
- return Utils.displayMetrics.heightPixels;
- }
-
- /**
- * Manually set the popup window size
- *
- * @param width Width of the popup
- * @param height Height of the popup
- */
- public void setSize(int width, int height) {
- setWidth(width);
- setHeight(height);
- }
-
- private View createCustomView() {
- final EmojiPicker emojiPicker = new EmojiPicker(context);
- final LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
- emojiPicker.setLayoutParams(layoutParams);
- emojiPicker.init(rootView, onEmojiClickListener, onBackspaceClickListener);
- return emojiPicker;
- }
-
-
- public interface OnSoftKeyboardOpenCloseListener {
- void onKeyboardOpen(int keyBoardHeight);
-
- void onKeyboardClose();
- }
-}
-
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiVariantManager.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiVariantManager.java
index 95eb3330..28a39d26 100644
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiVariantManager.java
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiVariantManager.java
@@ -21,7 +21,7 @@ public class EmojiVariantManager {
private static final String TAG = EmojiVariantManager.class.getSimpleName();
private static final Object LOCK = new Object();
- private final AppExecutors appExecutors = AppExecutors.getInstance();
+ private final AppExecutors appExecutors = AppExecutors.INSTANCE;
private final Map selectedVariantMap = new HashMap<>();
private static EmojiVariantManager instance;
@@ -57,7 +57,7 @@ public class EmojiVariantManager {
public void setVariant(final String parent, final String variant) {
if (parent == null || variant == null) return;
selectedVariantMap.put(parent, variant);
- appExecutors.tasksThread().execute(() -> {
+ appExecutors.getTasksThread().execute(() -> {
final JSONObject jsonObject = new JSONObject(selectedVariantMap);
final String json = jsonObject.toString();
Utils.settingsHelper.putString(PREF_EMOJI_VARIANTS, json);
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiVariantPopup.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiVariantPopup.java
index b0de3b6c..0d186256 100644
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiVariantPopup.java
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiVariantPopup.java
@@ -57,7 +57,7 @@ public final class EmojiVariantPopup {
this.rootView = rootView;
this.listener = listener;
emojiVariantManager = EmojiVariantManager.getInstance();
- appExecutors = AppExecutors.getInstance();
+ appExecutors = AppExecutors.INSTANCE;
}
public void show(@NonNull final View view, @NonNull final Emoji emoji) {
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java b/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java
index f2bec738..ae1aff00 100644
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java
@@ -25,7 +25,6 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.Spanned;
import android.text.TextPaint;
-import android.util.Log;
import androidx.annotation.NonNull;
import androidx.emoji.text.EmojiCompat;
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/ReactionsManager.java b/app/src/main/java/awais/instagrabber/customviews/emoji/ReactionsManager.java
index 0e812528..d75ba8cc 100644
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/ReactionsManager.java
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/ReactionsManager.java
@@ -1,7 +1,10 @@
package awais.instagrabber.customviews.emoji;
+import android.content.Context;
import android.util.Log;
+import androidx.annotation.NonNull;
+
import com.google.common.collect.ImmutableList;
import org.json.JSONArray;
@@ -11,7 +14,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.utils.emoji.EmojiParser;
@@ -22,29 +24,29 @@ public class ReactionsManager {
private static final String TAG = ReactionsManager.class.getSimpleName();
private static final Object LOCK = new Object();
- private final AppExecutors appExecutors = AppExecutors.getInstance();
+ // private final AppExecutors appExecutors = AppExecutors.INSTANCE;
private final List reactions = new ArrayList<>();
private static ReactionsManager instance;
- public static ReactionsManager getInstance() {
+ public static ReactionsManager getInstance(@NonNull final Context context) {
if (instance == null) {
synchronized (LOCK) {
if (instance == null) {
- instance = new ReactionsManager();
+ instance = new ReactionsManager(context);
}
}
}
return instance;
}
- private ReactionsManager() {
+ private ReactionsManager(@NonNull final Context context) {
+ final EmojiParser emojiParser = EmojiParser.Companion.getInstance(context);
String reactionsJson = Utils.settingsHelper.getString(PREF_REACTIONS);
if (TextUtils.isEmpty(reactionsJson)) {
final ImmutableList list = ImmutableList.of("❤️", "\uD83D\uDE02", "\uD83D\uDE2E", "\uD83D\uDE22", "\uD83D\uDE21", "\uD83D\uDC4D");
reactionsJson = new JSONArray(list).toString();
}
- final EmojiParser emojiParser = EmojiParser.getInstance();
final Map allEmojis = emojiParser.getAllEmojis();
try {
final JSONArray reactionsJsonArray = new JSONArray(reactionsJson);
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/ChangeText.java b/app/src/main/java/awais/instagrabber/customviews/helpers/ChangeText.java
new file mode 100644
index 00000000..bd3613ec
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/ChangeText.java
@@ -0,0 +1,320 @@
+package awais.instagrabber.customviews.helpers;
+
+/*
+ * Copyright (C) 2013 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.
+ */
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.graphics.Color;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.transition.Transition;
+import androidx.transition.TransitionListenerAdapter;
+import androidx.transition.TransitionValues;
+
+import java.util.Map;
+import java.util.Objects;
+
+import awais.instagrabber.BuildConfig;
+
+/**
+ * This transition tracks changes to the text in TextView targets. If the text
+ * changes between the start and end scenes, the transition ensures that the
+ * starting text stays until the transition ends, at which point it changes
+ * to the end text. This is useful in situations where you want to resize a
+ * text view to its new size before displaying the text that goes there.
+ */
+public class ChangeText extends Transition {
+ private static final String LOG_TAG = "TextChange";
+ private static final String PROPNAME_TEXT = "android:textchange:text";
+ private static final String PROPNAME_TEXT_SELECTION_START =
+ "android:textchange:textSelectionStart";
+ private static final String PROPNAME_TEXT_SELECTION_END =
+ "android:textchange:textSelectionEnd";
+ private static final String PROPNAME_TEXT_COLOR = "android:textchange:textColor";
+ private int mChangeBehavior = CHANGE_BEHAVIOR_KEEP;
+ private boolean crossFade;
+ /**
+ * Flag specifying that the text in affected/changing TextView targets will keep
+ * their original text during the transition, setting it to the final text when
+ * the transition ends. This is the default behavior.
+ *
+ * @see #setChangeBehavior(int)
+ */
+ public static final int CHANGE_BEHAVIOR_KEEP = 0;
+ /**
+ * Flag specifying that the text changing animation should first fade
+ * out the original text completely. The new text is set on the target
+ * view at the end of the fade-out animation. This transition is typically
+ * used with a later {@link #CHANGE_BEHAVIOR_IN} transition, allowing more
+ * flexibility than the {@link #CHANGE_BEHAVIOR_OUT_IN} by allowing other
+ * transitions to be run sequentially or in parallel with these fades.
+ *
+ * @see #setChangeBehavior(int)
+ */
+ public static final int CHANGE_BEHAVIOR_OUT = 1;
+ /**
+ * Flag specifying that the text changing animation should fade in the
+ * end text into the affected target view(s). This transition is typically
+ * used in conjunction with an earlier {@link #CHANGE_BEHAVIOR_OUT}
+ * transition, possibly with other transitions running as well, such as
+ * a sequence to fade out, then resize the view, then fade in.
+ *
+ * @see #setChangeBehavior(int)
+ */
+ public static final int CHANGE_BEHAVIOR_IN = 2;
+ /**
+ * Flag specifying that the text changing animation should first fade
+ * out the original text completely and then fade in the
+ * new text.
+ *
+ * @see #setChangeBehavior(int)
+ */
+ public static final int CHANGE_BEHAVIOR_OUT_IN = 3;
+ private static final String[] sTransitionProperties = {
+ PROPNAME_TEXT,
+ PROPNAME_TEXT_SELECTION_START,
+ PROPNAME_TEXT_SELECTION_END
+ };
+
+ /**
+ * Sets the type of changing animation that will be run, one of
+ * {@link #CHANGE_BEHAVIOR_KEEP}, {@link #CHANGE_BEHAVIOR_OUT},
+ * {@link #CHANGE_BEHAVIOR_IN}, and {@link #CHANGE_BEHAVIOR_OUT_IN}.
+ *
+ * @param changeBehavior The type of fading animation to use when this
+ * transition is run.
+ * @return this textChange object.
+ */
+ public ChangeText setChangeBehavior(int changeBehavior) {
+ if (changeBehavior >= CHANGE_BEHAVIOR_KEEP && changeBehavior <= CHANGE_BEHAVIOR_OUT_IN) {
+ mChangeBehavior = changeBehavior;
+ }
+ return this;
+ }
+
+ public ChangeText setCrossFade(final boolean crossFade) {
+ this.crossFade = crossFade;
+ return this;
+ }
+
+ @Override
+ public String[] getTransitionProperties() {
+ return sTransitionProperties;
+ }
+
+ /**
+ * Returns the type of changing animation that will be run.
+ *
+ * @return either {@link #CHANGE_BEHAVIOR_KEEP}, {@link #CHANGE_BEHAVIOR_OUT},
+ * {@link #CHANGE_BEHAVIOR_IN}, or {@link #CHANGE_BEHAVIOR_OUT_IN}.
+ */
+ public int getChangeBehavior() {
+ return mChangeBehavior;
+ }
+
+ private void captureValues(TransitionValues transitionValues) {
+ if (transitionValues.view instanceof TextView) {
+ TextView textview = (TextView) transitionValues.view;
+ transitionValues.values.put(PROPNAME_TEXT, textview.getText());
+ if (textview instanceof EditText) {
+ transitionValues.values.put(PROPNAME_TEXT_SELECTION_START,
+ textview.getSelectionStart());
+ transitionValues.values.put(PROPNAME_TEXT_SELECTION_END,
+ textview.getSelectionEnd());
+ }
+ if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) {
+ transitionValues.values.put(PROPNAME_TEXT_COLOR, textview.getCurrentTextColor());
+ }
+ }
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ if (startValues == null || endValues == null ||
+ !(startValues.view instanceof TextView) || !(endValues.view instanceof TextView)) {
+ return null;
+ }
+ final TextView view = (TextView) endValues.view;
+ Map startVals = startValues.values;
+ Map endVals = endValues.values;
+ final CharSequence startText = startVals.get(PROPNAME_TEXT) != null ?
+ (CharSequence) startVals.get(PROPNAME_TEXT) : "";
+ final CharSequence endText = endVals.get(PROPNAME_TEXT) != null ?
+ (CharSequence) endVals.get(PROPNAME_TEXT) : "";
+ final int startSelectionStart, startSelectionEnd, endSelectionStart, endSelectionEnd;
+ if (view instanceof EditText) {
+ startSelectionStart = startVals.get(PROPNAME_TEXT_SELECTION_START) != null ?
+ (Integer) startVals.get(PROPNAME_TEXT_SELECTION_START) : -1;
+ startSelectionEnd = startVals.get(PROPNAME_TEXT_SELECTION_END) != null ?
+ (Integer) startVals.get(PROPNAME_TEXT_SELECTION_END) : startSelectionStart;
+ endSelectionStart = endVals.get(PROPNAME_TEXT_SELECTION_START) != null ?
+ (Integer) endVals.get(PROPNAME_TEXT_SELECTION_START) : -1;
+ endSelectionEnd = endVals.get(PROPNAME_TEXT_SELECTION_END) != null ?
+ (Integer) endVals.get(PROPNAME_TEXT_SELECTION_END) : endSelectionStart;
+ } else {
+ startSelectionStart = startSelectionEnd = endSelectionStart = endSelectionEnd = -1;
+ }
+ if (!Objects.equals(startText, endText)) {
+ final int startColor;
+ final int endColor;
+ if (mChangeBehavior != CHANGE_BEHAVIOR_IN) {
+ view.setText(startText);
+ if (view instanceof EditText) {
+ setSelection(((EditText) view), startSelectionStart, startSelectionEnd);
+ }
+ }
+ Animator anim;
+ if (mChangeBehavior == CHANGE_BEHAVIOR_KEEP) {
+ startColor = endColor = 0;
+ anim = ValueAnimator.ofFloat(0, 1);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (Objects.equals(startText, view.getText())) {
+ // Only set if it hasn't been changed since anim started
+ view.setText(endText);
+ if (view instanceof EditText) {
+ setSelection(((EditText) view), endSelectionStart, endSelectionEnd);
+ }
+ }
+ }
+ });
+ } else {
+ startColor = (Integer) startVals.get(PROPNAME_TEXT_COLOR);
+ endColor = (Integer) endVals.get(PROPNAME_TEXT_COLOR);
+ // Fade out start text
+ ValueAnimator outAnim = null, inAnim = null;
+ if (mChangeBehavior == CHANGE_BEHAVIOR_OUT_IN ||
+ mChangeBehavior == CHANGE_BEHAVIOR_OUT) {
+ outAnim = ValueAnimator.ofInt(Color.alpha(startColor), 0);
+ outAnim.addUpdateListener(animation -> {
+ int currAlpha = (Integer) animation.getAnimatedValue();
+ view.setTextColor(currAlpha << 24 | startColor & 0xffffff);
+ });
+ outAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (Objects.equals(startText, view.getText())) {
+ // Only set if it hasn't been changed since anim started
+ view.setText(endText);
+ if (view instanceof EditText) {
+ setSelection(((EditText) view), endSelectionStart,
+ endSelectionEnd);
+ }
+ }
+ // restore opaque alpha and correct end color
+ view.setTextColor(endColor);
+ }
+ });
+ }
+ if (mChangeBehavior == CHANGE_BEHAVIOR_OUT_IN ||
+ mChangeBehavior == CHANGE_BEHAVIOR_IN) {
+ inAnim = ValueAnimator.ofInt(0, Color.alpha(endColor));
+ inAnim.addUpdateListener(animation -> {
+ int currAlpha = (Integer) animation.getAnimatedValue();
+ view.setTextColor(currAlpha << 24 | endColor & 0xffffff);
+ });
+ inAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ // restore opaque alpha and correct end color
+ view.setTextColor(endColor);
+ }
+ });
+ }
+ if (outAnim != null && inAnim != null) {
+ anim = new AnimatorSet();
+ final AnimatorSet animatorSet = (AnimatorSet) anim;
+ if (crossFade) {
+ animatorSet.playTogether(outAnim, inAnim);
+ } else {
+ animatorSet.playSequentially(outAnim, inAnim);
+ }
+ } else if (outAnim != null) {
+ anim = outAnim;
+ } else {
+ // Must be an in-only animation
+ anim = inAnim;
+ }
+ }
+ TransitionListener transitionListener = new TransitionListenerAdapter() {
+ int mPausedColor = 0;
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ if (mChangeBehavior != CHANGE_BEHAVIOR_IN) {
+ view.setText(endText);
+ if (view instanceof EditText) {
+ setSelection(((EditText) view), endSelectionStart, endSelectionEnd);
+ }
+ }
+ if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) {
+ mPausedColor = view.getCurrentTextColor();
+ view.setTextColor(endColor);
+ }
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ if (mChangeBehavior != CHANGE_BEHAVIOR_IN) {
+ view.setText(startText);
+ if (view instanceof EditText) {
+ setSelection(((EditText) view), startSelectionStart, startSelectionEnd);
+ }
+ }
+ if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) {
+ view.setTextColor(mPausedColor);
+ }
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ transition.removeListener(this);
+ }
+ };
+ addListener(transitionListener);
+ if (BuildConfig.DEBUG) {
+ Log.d(LOG_TAG, "createAnimator returning " + anim);
+ }
+ return anim;
+ }
+ return null;
+ }
+
+ private void setSelection(EditText editText, int start, int end) {
+ if (start >= 0 && end >= 0) {
+ editText.setSelection(start, end);
+ }
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java
new file mode 100644
index 00000000..e1fda461
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2020 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.customviews.helpers;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsAnimationCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import java.util.List;
+
+/**
+ * A [WindowInsetsAnimationCompat.Callback] which will request and clear focus on the given view,
+ * depending on the [WindowInsetsCompat.Type.ime] visibility state when an IME
+ * [WindowInsetsAnimationCompat] has finished.
+ *
+ * This is primarily used when animating the [WindowInsetsCompat.Type.ime], so that the
+ * appropriate view is focused for accepting input from the IME.
+ */
+public class ControlFocusInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback {
+
+ private final View view;
+
+ public ControlFocusInsetsAnimationCallback(@NonNull final View view) {
+ this(view, DISPATCH_MODE_STOP);
+ }
+
+ /**
+ * @param view the view to request/clear focus
+ * @param dispatchMode The dispatch mode for this callback.
+ * @see WindowInsetsAnimationCompat.Callback.DispatchMode
+ */
+ public ControlFocusInsetsAnimationCallback(@NonNull final View view, final int dispatchMode) {
+ super(dispatchMode);
+ this.view = view;
+ }
+
+ @NonNull
+ @Override
+ public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets,
+ @NonNull final List runningAnimations) {
+ // no-op and return the insets
+ return insets;
+ }
+
+ @Override
+ public void onEnd(final WindowInsetsAnimationCompat animation) {
+ if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
+ // The animation has now finished, so we can check the view's focus state.
+ // We post the check because the rootWindowInsets has not yet been updated, but will
+ // be in the next message traversal
+ view.post(this::checkFocus);
+ }
+ }
+
+ private void checkFocus() {
+ final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view);
+ boolean imeVisible = false;
+ if (rootWindowInsets != null) {
+ imeVisible = rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime());
+ }
+ if (imeVisible && view.getRootView().findFocus() == null) {
+ // If the IME will be visible, and there is not a currently focused view in
+ // the hierarchy, request focus on our view
+ view.requestFocus();
+ } else if (!imeVisible && view.isFocused()) {
+ // If the IME will not be visible and our view is currently focused, clear the focus
+ view.clearFocus();
+ }
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/CustomHideBottomViewOnScrollBehavior.java b/app/src/main/java/awais/instagrabber/customviews/helpers/CustomHideBottomViewOnScrollBehavior.java
index 765fc679..dabc622b 100644
--- a/app/src/main/java/awais/instagrabber/customviews/helpers/CustomHideBottomViewOnScrollBehavior.java
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/CustomHideBottomViewOnScrollBehavior.java
@@ -1,5 +1,7 @@
package awais.instagrabber.customviews.helpers;
+import android.content.Context;
+import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
@@ -12,6 +14,13 @@ import com.google.android.material.bottomnavigation.BottomNavigationView;
public class CustomHideBottomViewOnScrollBehavior extends HideBottomViewOnScrollBehavior {
private static final String TAG = "CustomHideBottomView";
+ public CustomHideBottomViewOnScrollBehavior() {
+ }
+
+ public CustomHideBottomViewOnScrollBehavior(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
@Override
public boolean onStartNestedScroll(@NonNull final CoordinatorLayout coordinatorLayout,
@NonNull final BottomNavigationView child,
@@ -23,7 +32,13 @@ public class CustomHideBottomViewOnScrollBehavior extends HideBottomViewOnScroll
}
@Override
- public void onNestedPreScroll(@NonNull final CoordinatorLayout coordinatorLayout, @NonNull final BottomNavigationView child, @NonNull final View target, final int dx, final int dy, @NonNull final int[] consumed, final int type) {
+ public void onNestedPreScroll(@NonNull final CoordinatorLayout coordinatorLayout,
+ @NonNull final BottomNavigationView child,
+ @NonNull final View target,
+ final int dx,
+ final int dy,
+ @NonNull final int[] consumed,
+ final int type) {
if (dy > 0) {
slideDown(child);
} else if (dy < 0) {
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java
new file mode 100644
index 00000000..125b65c1
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java
@@ -0,0 +1,117 @@
+package awais.instagrabber.customviews.helpers;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsAnimationCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import java.util.List;
+
+/**
+ * A customized {@link TranslateDeferringInsetsAnimationCallback} for the emoji picker
+ */
+public class EmojiPickerInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback {
+ private static final String TAG = EmojiPickerInsetsAnimationCallback.class.getSimpleName();
+
+ private final View view;
+ private final int persistentInsetTypes;
+ private final int deferredInsetTypes;
+
+ private int kbHeight;
+ private onKbVisibilityChangeListener listener;
+ private boolean shouldTranslate;
+
+ public EmojiPickerInsetsAnimationCallback(final View view,
+ final int persistentInsetTypes,
+ final int deferredInsetTypes) {
+ this(view, persistentInsetTypes, deferredInsetTypes, DISPATCH_MODE_STOP);
+ }
+
+ public EmojiPickerInsetsAnimationCallback(final View view,
+ final int persistentInsetTypes,
+ final int deferredInsetTypes,
+ final int dispatchMode) {
+ super(dispatchMode);
+ if ((persistentInsetTypes & deferredInsetTypes) != 0) {
+ throw new IllegalArgumentException("persistentInsetTypes and deferredInsetTypes can not contain " +
+ "any of same WindowInsetsCompat.Type values");
+ }
+ this.view = view;
+ this.persistentInsetTypes = persistentInsetTypes;
+ this.deferredInsetTypes = deferredInsetTypes;
+ }
+
+ @NonNull
+ @Override
+ public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets,
+ @NonNull final List runningAnimations) {
+ // onProgress() is called when any of the running animations progress...
+
+ // First we get the insets which are potentially deferred
+ final Insets typesInset = insets.getInsets(deferredInsetTypes);
+ // Then we get the persistent inset types which are applied as padding during layout
+ final Insets otherInset = insets.getInsets(persistentInsetTypes);
+
+ // Now that we subtract the two insets, to calculate the difference. We also coerce
+ // the insets to be >= 0, to make sure we don't use negative insets.
+ final Insets subtract = Insets.subtract(typesInset, otherInset);
+ final Insets diff = Insets.max(subtract, Insets.NONE);
+
+ // The resulting `diff` insets contain the values for us to apply as a translation
+ // to the view
+ view.setTranslationX(diff.left - diff.right);
+ view.setTranslationY(shouldTranslate ? diff.top - diff.bottom : -kbHeight);
+
+ return insets;
+ }
+
+ @Override
+ public void onEnd(@NonNull final WindowInsetsAnimationCompat animation) {
+ try {
+ final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view);
+ if (kbHeight == 0) {
+ if (rootWindowInsets == null) return;
+ final Insets imeInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime());
+ final Insets navBarInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.navigationBars());
+ kbHeight = imeInsets.bottom - navBarInsets.bottom;
+ final ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
+ if (layoutParams != null) {
+ layoutParams.height = kbHeight;
+ layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin, layoutParams.rightMargin, -kbHeight);
+ }
+ }
+ view.setTranslationX(0f);
+ final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime());
+ float translationY = 0;
+ if (!shouldTranslate) {
+ translationY = -kbHeight;
+ if (visible) {
+ translationY = 0;
+ }
+ }
+ view.setTranslationY(translationY);
+
+ if (listener != null && rootWindowInsets != null) {
+ listener.onChange(visible);
+ }
+ } finally {
+ shouldTranslate = true;
+ }
+ }
+
+ public void setShouldTranslate(final boolean shouldTranslate) {
+ this.shouldTranslate = shouldTranslate;
+ }
+
+ public void setKbVisibilityListener(final onKbVisibilityChangeListener listener) {
+ this.listener = listener;
+ }
+
+ public interface onKbVisibilityChangeListener {
+ void onChange(boolean isVisible);
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java b/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java
index 81b4be92..f34c30f5 100755
--- a/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java
@@ -7,17 +7,24 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
- private final int spacing;
+ private final int halfSpace;
+
+ private boolean hasHeader;
public GridSpacingItemDecoration(int spacing) {
- this.spacing = spacing;
+ halfSpace = spacing / 2;
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
- final int halfSpace = spacing / 2;
+ if (hasHeader && parent.getChildAdapterPosition(view) == 0) {
+ outRect.bottom = halfSpace;
+ outRect.left = -halfSpace;
+ outRect.right = -halfSpace;
+ return;
+ }
if (parent.getPaddingLeft() != halfSpace) {
- parent.setPadding(halfSpace, halfSpace, halfSpace, halfSpace);
+ parent.setPadding(halfSpace, hasHeader ? 0 : halfSpace, halfSpace, halfSpace);
parent.setClipToPadding(false);
}
outRect.top = halfSpace;
@@ -25,4 +32,8 @@ public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
outRect.left = halfSpace;
outRect.right = halfSpace;
}
+
+ public void setHasHeader(final boolean hasHeader) {
+ this.hasHeader = hasHeader;
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java
new file mode 100644
index 00000000..f58be88a
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java
@@ -0,0 +1,139 @@
+package awais.instagrabber.customviews.helpers;/*
+ * Copyright 2020 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.
+ */
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.Insets;
+import androidx.core.view.OnApplyWindowInsetsListener;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsAnimationCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import java.util.List;
+
+/**
+ * A class which extends/implements both [WindowInsetsAnimationCompat.Callback] and
+ * [View.OnApplyWindowInsetsListener], which should be set on the root view in your layout.
+ *
+ * This class enables the root view is selectively defer handling any insets which match
+ * [deferredInsetTypes], to enable better looking [WindowInsetsAnimationCompat]s.
+ *
+ * An example is the following: when a [WindowInsetsAnimationCompat] is started, the system will dispatch
+ * a [WindowInsetsCompat] instance which contains the end state of the animation. For the scenario of
+ * the IME being animated in, that means that the insets contains the IME height. If the view's
+ * [View.OnApplyWindowInsetsListener] simply always applied the combination of
+ * [WindowInsetsCompat.Type.ime] and [WindowInsetsCompat.Type.systemBars] using padding, the viewport of any
+ * child views would then be smaller. This results in us animating a smaller (padded-in) view into
+ * a larger viewport. Visually, this results in the views looking clipped.
+ *
+ * This class allows us to implement a different strategy for the above scenario, by selectively
+ * deferring the [WindowInsetsCompat.Type.ime] insets until the [WindowInsetsAnimationCompat] is ended.
+ * For the above example, you would create a [RootViewDeferringInsetsCallback] like so:
+ *
+ * This class is not limited to just IME animations, and can work with any [WindowInsetsCompat.Type]s.
+ */
+public class RootViewDeferringInsetsCallback extends WindowInsetsAnimationCompat.Callback implements OnApplyWindowInsetsListener {
+
+ private final int persistentInsetTypes;
+ private final int deferredInsetTypes;
+ @Nullable
+ private View view = null;
+ @Nullable
+ private WindowInsetsCompat lastWindowInsets = null;
+ private boolean deferredInsets = false;
+
+ /**
+ * @param persistentInsetTypes the bitmask of any inset types which should always be handled
+ * through padding the attached view
+ * @param deferredInsetTypes the bitmask of insets types which should be deferred until after
+ * any related [WindowInsetsAnimationCompat]s have ended
+ */
+ public RootViewDeferringInsetsCallback(final int persistentInsetTypes, final int deferredInsetTypes) {
+ super(DISPATCH_MODE_CONTINUE_ON_SUBTREE);
+ if ((persistentInsetTypes & deferredInsetTypes) != 0) {
+ throw new IllegalArgumentException("persistentInsetTypes and deferredInsetTypes can not contain " +
+ "any of same WindowInsetsCompat.Type values");
+ }
+ this.persistentInsetTypes = persistentInsetTypes;
+ this.deferredInsetTypes = deferredInsetTypes;
+ }
+
+ @Override
+ public WindowInsetsCompat onApplyWindowInsets(@NonNull final View v, @NonNull final WindowInsetsCompat windowInsets) {
+ // Store the view and insets for us in onEnd() below
+ view = v;
+ lastWindowInsets = windowInsets;
+
+ final int types = deferredInsets
+ // When the deferred flag is enabled, we only use the systemBars() insets
+ ? persistentInsetTypes
+ // Otherwise we handle the combination of the the systemBars() and ime() insets
+ : persistentInsetTypes | deferredInsetTypes;
+
+ // Finally we apply the resolved insets by setting them as padding
+ final Insets typeInsets = windowInsets.getInsets(types);
+ v.setPadding(typeInsets.left, typeInsets.top, typeInsets.right, typeInsets.bottom);
+
+ // We return the new WindowInsetsCompat.CONSUMED to stop the insets being dispatched any
+ // further into the view hierarchy. This replaces the deprecated
+ // WindowInsetsCompat.consumeSystemWindowInsets() and related functions.
+ return WindowInsetsCompat.CONSUMED;
+ }
+
+ @Override
+ public void onPrepare(WindowInsetsAnimationCompat animation) {
+ if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
+ // We defer the WindowInsetsCompat.Type.ime() insets if the IME is currently not visible.
+ // This results in only the WindowInsetsCompat.Type.systemBars() being applied, allowing
+ // the scrolling view to remain at it's larger size.
+ deferredInsets = true;
+ }
+ }
+
+ @NonNull
+ @Override
+ public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets,
+ @NonNull final List runningAnims) {
+ // This is a no-op. We don't actually want to handle any WindowInsetsAnimations
+ return insets;
+ }
+
+ @Override
+ public void onEnd(@NonNull final WindowInsetsAnimationCompat animation) {
+ if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) {
+ // If we deferred the IME insets and an IME animation has finished, we need to reset
+ // the flag
+ deferredInsets = false;
+
+ // And finally dispatch the deferred insets to the view now.
+ // Ideally we would just call view.requestApplyInsets() and let the normal dispatch
+ // cycle happen, but this happens too late resulting in a visual flicker.
+ // Instead we manually dispatch the most recent WindowInsets to the view.
+ if (lastWindowInsets != null && view != null) {
+ ViewCompat.dispatchApplyWindowInsets(view, lastWindowInsets);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java b/app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java
new file mode 100644
index 00000000..9bcc24d8
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright 2020 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.customviews.helpers;
+
+import android.os.CancellationSignal;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsAnimationControlListenerCompat;
+import androidx.core.view.WindowInsetsAnimationControllerCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.WindowInsetsControllerCompat;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import awais.instagrabber.utils.ViewUtils;
+
+/**
+ * A wrapper around the [WindowInsetsAnimationControllerCompat] APIs in AndroidX Core, to simplify
+ * the implementation of common use-cases around the IME.
+ *
+ * See [InsetsAnimationLinearLayout] and [InsetsAnimationTouchListener] for examples of how
+ * to use this class.
+ */
+public class SimpleImeAnimationController {
+ private static final String TAG = SimpleImeAnimationController.class.getSimpleName();
+ /**
+ * Scroll threshold for determining whether to animating to the end state, or to the start state.
+ * Currently 15% of the total swipe distance distance
+ */
+ private static final float SCROLL_THRESHOLD = 0.15f;
+
+ @Nullable
+ private WindowInsetsAnimationControllerCompat insetsAnimationController = null;
+ @Nullable
+ private CancellationSignal pendingRequestCancellationSignal = null;
+ @Nullable
+ private OnRequestReadyListener pendingRequestOnReadyListener;
+ /**
+ * True if the IME was shown at the start of the current animation.
+ */
+ private boolean isImeShownAtStart = false;
+ @Nullable
+ private SpringAnimation currentSpringAnimation = null;
+ private WindowInsetsAnimationControlListenerCompat fwdListener;
+
+ /**
+ * A LinearInterpolator instance we can re-use across listeners.
+ */
+ private final LinearInterpolator linearInterpolator = new LinearInterpolator();
+ /* To take control of the an WindowInsetsAnimation, we need to pass in a listener to
+ controlWindowInsetsAnimation() in startControlRequest(). The listener created here
+ keeps track of the current WindowInsetsAnimationController and resets our state. */
+ private final WindowInsetsAnimationControlListenerCompat animationControlListener = new WindowInsetsAnimationControlListenerCompat() {
+ /**
+ * Once the request is ready, call our [onRequestReady] function
+ */
+ @Override
+ public void onReady(@NonNull final WindowInsetsAnimationControllerCompat controller, final int types) {
+ onRequestReady(controller);
+ if (fwdListener != null) {
+ fwdListener.onReady(controller, types);
+ }
+ }
+
+ /**
+ * If the request is finished, we should reset our internal state
+ */
+ @Override
+ public void onFinished(@NonNull final WindowInsetsAnimationControllerCompat controller) {
+ reset();
+ if (fwdListener != null) {
+ fwdListener.onFinished(controller);
+ }
+ }
+
+ /**
+ * If the request is cancelled, we should reset our internal state
+ */
+ @Override
+ public void onCancelled(@Nullable final WindowInsetsAnimationControllerCompat controller) {
+ reset();
+ if (fwdListener != null) {
+ fwdListener.onCancelled(controller);
+ }
+ }
+ };
+
+ /**
+ * Start a control request to the [view]s [android.view.WindowInsetsController]. This should
+ * be called once the view is in a position to take control over the position of the IME.
+ *
+ * @param view The view which is triggering this request
+ * @param onRequestReadyListener optional listener which will be called when the request is ready and
+ * the animation can proceed
+ */
+ public void startControlRequest(@NonNull final View view,
+ @Nullable final OnRequestReadyListener onRequestReadyListener) {
+ if (isInsetAnimationInProgress()) {
+ Log.w(TAG, "startControlRequest: Animation in progress. Can not start a new request to controlWindowInsetsAnimation()");
+ return;
+ }
+
+ // Keep track of the IME insets, and the IME visibility, at the start of the request
+ final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view);
+ if (rootWindowInsets != null) {
+ isImeShownAtStart = rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime());
+ }
+
+ // Create a cancellation signal, which we pass to controlWindowInsetsAnimation() below
+ pendingRequestCancellationSignal = new CancellationSignal();
+ // Keep reference to the onReady callback
+ pendingRequestOnReadyListener = onRequestReadyListener;
+
+ // Finally we make a controlWindowInsetsAnimation() request:
+ final WindowInsetsControllerCompat windowInsetsController = ViewCompat.getWindowInsetsController(view);
+ if (windowInsetsController != null) {
+ windowInsetsController.controlWindowInsetsAnimation(
+ // We're only catering for IME animations in this listener
+ WindowInsetsCompat.Type.ime(),
+ // Animation duration. This is not used by the system, and is only passed to any
+ // WindowInsetsAnimation.Callback set on views. We pass in -1 to indicate that we're
+ // not starting a finite animation, and that this is completely controlled by
+ // the user's touch.
+ -1,
+ // The time interpolator used in calculating the animation progress. The fraction value
+ // we passed into setInsetsAndAlpha() which be passed into this interpolator before
+ // being used by the system to inset the IME. LinearInterpolator is a good type
+ // to use for scrolling gestures.
+ linearInterpolator,
+ // A cancellation signal, which allows us to cancel the request to control
+ pendingRequestCancellationSignal,
+ // The WindowInsetsAnimationControlListener
+ animationControlListener
+ );
+ }
+ }
+
+ /**
+ * Start a control request to the [view]s [android.view.WindowInsetsController], similar to
+ * [startControlRequest], but immediately fling to a finish using [velocityY] once ready.
+ *
+ * This function is useful for fire-and-forget operations to animate the IME.
+ *
+ * @param view The view which is triggering this request
+ * @param velocityY the velocity of the touch gesture which caused this call
+ */
+ public void startAndFling(@NonNull final View view, final float velocityY) {
+ startControlRequest(view, null);
+ animateToFinish(velocityY);
+ }
+
+ /**
+ * Update the inset position of the IME by the given [dy] value. This value will be coerced
+ * into the hidden and shown inset values.
+ *
+ * This function should only be called if [isInsetAnimationInProgress] returns true.
+ *
+ * @return the amount of [dy] consumed by the inset animation, in pixels
+ */
+ public int insetBy(final int dy) {
+ if (insetsAnimationController == null) {
+ throw new IllegalStateException("Current WindowInsetsAnimationController is null." +
+ "This should only be called if isAnimationInProgress() returns true");
+ }
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ // Call updateInsetTo() with the new inset value
+ return insetTo(controller.getCurrentInsets().bottom - dy);
+ }
+
+ /**
+ * Update the inset position of the IME to be the given [inset] value. This value will be
+ * coerced into the hidden and shown inset values.
+ *
+ * This function should only be called if [isInsetAnimationInProgress] returns true.
+ *
+ * @return the distance moved by the inset animation, in pixels
+ */
+ public int insetTo(final int inset) {
+ if (insetsAnimationController == null) {
+ throw new IllegalStateException("Current WindowInsetsAnimationController is null." +
+ "This should only be called if isAnimationInProgress() returns true");
+ }
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ final int hiddenBottom = controller.getHiddenStateInsets().bottom;
+ final int shownBottom = controller.getShownStateInsets().bottom;
+ final int startBottom = isImeShownAtStart ? shownBottom : hiddenBottom;
+ final int endBottom = isImeShownAtStart ? hiddenBottom : shownBottom;
+
+ // We coerce the given inset within the limits of the hidden and shown insets
+ final int coercedBottom = coerceIn(inset, hiddenBottom, shownBottom);
+
+ final int consumedDy = controller.getCurrentInsets().bottom - coercedBottom;
+
+ // Finally update the insets in the WindowInsetsAnimationController using
+ // setInsetsAndAlpha().
+ controller.setInsetsAndAlpha(
+ // Here we update the animating insets. This is what controls where the IME is displayed.
+ // It is also passed through to views via their WindowInsetsAnimation.Callback.
+ Insets.of(0, 0, 0, coercedBottom),
+ // This controls the alpha value. We don't want to alter the alpha so use 1f
+ 1f,
+ // Finally we calculate the animation progress fraction. This value is passed through
+ // to any WindowInsetsAnimation.Callbacks, but it is not used by the system.
+ (coercedBottom - startBottom) / (float) (endBottom - startBottom)
+ );
+
+ return consumedDy;
+ }
+
+ /**
+ * Return `true` if an inset animation is in progress.
+ */
+ public boolean isInsetAnimationInProgress() {
+ return insetsAnimationController != null;
+ }
+
+ /**
+ * Return `true` if an inset animation is currently finishing.
+ */
+ public boolean isInsetAnimationFinishing() {
+ return currentSpringAnimation != null;
+ }
+
+ /**
+ * Return `true` if a request to control an inset animation is in progress.
+ */
+ public boolean isInsetAnimationRequestPending() {
+ return pendingRequestCancellationSignal != null;
+ }
+
+ /**
+ * Cancel the current [WindowInsetsAnimationControllerCompat]. We immediately finish
+ * the animation, reverting back to the state at the start of the gesture.
+ */
+ public void cancel() {
+ if (insetsAnimationController != null) {
+ insetsAnimationController.finish(isImeShownAtStart);
+ }
+ if (pendingRequestCancellationSignal != null) {
+ pendingRequestCancellationSignal.cancel();
+ }
+ if (currentSpringAnimation != null) {
+ // Cancel the current spring animation
+ currentSpringAnimation.cancel();
+ }
+ reset();
+ }
+
+ /**
+ * Finish the current [WindowInsetsAnimationControllerCompat] immediately.
+ */
+ public void finish() {
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ if (controller == null) {
+ // If we don't currently have a controller, cancel any pending request and return
+ if (pendingRequestCancellationSignal != null) {
+ pendingRequestCancellationSignal.cancel();
+ }
+ return;
+ }
+
+ final int current = controller.getCurrentInsets().bottom;
+ final int shown = controller.getShownStateInsets().bottom;
+ final int hidden = controller.getHiddenStateInsets().bottom;
+
+ // The current inset matches either the shown/hidden inset, finish() immediately
+ if (current == shown) {
+ controller.finish(true);
+ } else if (current == hidden) {
+ controller.finish(false);
+ } else {
+ // Otherwise, we'll look at the current position...
+ if (controller.getCurrentFraction() >= SCROLL_THRESHOLD) {
+ // If the IME is past the 'threshold' we snap to the toggled state
+ controller.finish(!isImeShownAtStart);
+ } else {
+ // ...otherwise, we snap back to the original visibility
+ controller.finish(isImeShownAtStart);
+ }
+ }
+ }
+
+ /**
+ * Finish the current [WindowInsetsAnimationControllerCompat]. We finish the animation,
+ * animating to the end state if necessary.
+ *
+ * @param velocityY the velocity of the touch gesture which caused this call to [animateToFinish].
+ * Can be `null` if velocity is not available.
+ */
+ public void animateToFinish(@Nullable final Float velocityY) {
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ if (controller == null) {
+ // If we don't currently have a controller, cancel any pending request and return
+ if (pendingRequestCancellationSignal != null) {
+ pendingRequestCancellationSignal.cancel();
+ }
+ return;
+ }
+
+ final int current = controller.getCurrentInsets().bottom;
+ final int shown = controller.getShownStateInsets().bottom;
+ final int hidden = controller.getHiddenStateInsets().bottom;
+
+ if (velocityY != null) {
+ // If we have a velocity, we can use it's direction to determine
+ // the visibility. Upwards == visible
+ animateImeToVisibility(velocityY > 0, velocityY);
+ } else if (current == shown) {
+ // The current inset matches either the shown/hidden inset, finish() immediately
+ controller.finish(true);
+ } else if (current == hidden) {
+ controller.finish(false);
+ } else {
+ // Otherwise, we'll look at the current position...
+ if (controller.getCurrentFraction() >= SCROLL_THRESHOLD) {
+ // If the IME is past the 'threshold' we animate it to the toggled state
+ animateImeToVisibility(!isImeShownAtStart, null);
+ } else {
+ // ...otherwise, we animate it back to the original visibility
+ animateImeToVisibility(isImeShownAtStart, null);
+ }
+ }
+ }
+
+ private void onRequestReady(@NonNull final WindowInsetsAnimationControllerCompat controller) {
+ // The request is ready, so clear out the pending cancellation signal
+ pendingRequestCancellationSignal = null;
+ // Store the current WindowInsetsAnimationController
+ insetsAnimationController = controller;
+
+ // Call any pending callback
+ if (pendingRequestOnReadyListener != null) {
+ pendingRequestOnReadyListener.onRequestReady(controller);
+ }
+ pendingRequestOnReadyListener = null;
+ }
+
+ /**
+ * Resets all of our internal state.
+ */
+ private void reset() {
+ // Clear all of our internal state
+ insetsAnimationController = null;
+ pendingRequestCancellationSignal = null;
+ isImeShownAtStart = false;
+ if (currentSpringAnimation != null) {
+ currentSpringAnimation.cancel();
+ }
+ currentSpringAnimation = null;
+ pendingRequestOnReadyListener = null;
+ }
+
+ /**
+ * Animate the IME to a given visibility.
+ *
+ * @param visible `true` to animate the IME to it's fully shown state, `false` to it's
+ * fully hidden state.
+ * @param velocityY the velocity of the touch gesture which caused this call. Can be `null`
+ * if velocity is not available.
+ */
+ private void animateImeToVisibility(final boolean visible, @Nullable final Float velocityY) {
+ if (insetsAnimationController == null) {
+ throw new IllegalStateException("Controller should not be null");
+ }
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ final FloatPropertyCompat