No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
695 changed files with 26933 additions and 27578 deletions
-
19.all-contributorsrc
-
5.codebeatsettings
-
1.github/ISSUE_TEMPLATE/ban_report.md
-
6.github/ISSUE_TEMPLATE/bug_report.md
-
11.github/ISSUE_TEMPLATE/config.yml
-
16.github/ISSUE_TEMPLATE/questions.md
-
17.github/workflows/github_nightly_release.yml
-
33.github/workflows/github_pre_release.yml
-
2.gitignore
-
2.idea/compiler.xml
-
1.idea/gradle.xml
-
2.idea/misc.xml
-
1.idea/runConfigurations.xml
-
42README.md
-
128app/build.gradle
-
227app/schemas/awais.instagrabber.db.AppDatabase/6.json
-
51app/src/androidTest/java/awais/instagrabber/db/MigrationTest.java
-
82app/src/androidTest/java/awais/instagrabber/db/dao/RecentSearchDaoTest.java
-
6app/src/github/res/values-cs/strings.xml
-
6app/src/github/res/values-de/strings.xml
-
6app/src/github/res/values-ko/strings.xml
-
6app/src/github/res/values-nl/strings.xml
-
2app/src/github/res/values-pl/strings.xml
-
6app/src/github/res/values-ru/strings.xml
-
6app/src/github/res/values-sv/strings.xml
-
6app/src/github/res/values-vi/strings.xml
-
6app/src/github/res/values-zh-rTW/strings.xml
-
45app/src/main/AndroidManifest.xml
-
91app/src/main/java/awais/instagrabber/InstaGrabberApplication.java
-
71app/src/main/java/awais/instagrabber/InstaGrabberApplication.kt
-
21app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.java
-
18app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.kt
-
281app/src/main/java/awais/instagrabber/activities/CameraActivity.java
-
248app/src/main/java/awais/instagrabber/activities/CameraActivity.kt
-
149app/src/main/java/awais/instagrabber/activities/DirectDownload.java
-
140app/src/main/java/awais/instagrabber/activities/Login.java
-
119app/src/main/java/awais/instagrabber/activities/Login.kt
-
973app/src/main/java/awais/instagrabber/activities/MainActivity.java
-
821app/src/main/java/awais/instagrabber/activities/MainActivity.kt
-
197app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java
-
28app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java
-
5app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java
-
4app/src/main/java/awais/instagrabber/adapters/FavoritesAdapter.java
-
9app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java
-
42app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java
-
2app/src/main/java/awais/instagrabber/adapters/LikesAdapter.java
-
4app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java
-
9app/src/main/java/awais/instagrabber/adapters/SavedCollectionsAdapter.java
-
33app/src/main/java/awais/instagrabber/adapters/SearchCategoryAdapter.java
-
215app/src/main/java/awais/instagrabber/adapters/SearchItemsAdapter.java
-
16app/src/main/java/awais/instagrabber/adapters/SliderCallbackAdapter.java
-
23app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java
-
77app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java
-
209app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java
-
52app/src/main/java/awais/instagrabber/adapters/viewholder/DiscoverViewHolder.java
-
20app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java
-
54app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java
-
6app/src/main/java/awais/instagrabber/adapters/viewholder/FilterViewHolder.java
-
21app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java
-
80app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java
-
77app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java
-
122app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java
-
3app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java
-
16app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java
-
95app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java
-
95app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java
-
4app/src/main/java/awais/instagrabber/adapters/viewholder/dialogs/KeywordsFilterDialogViewHolder.java
-
4app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java
-
9app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemActionLogViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
-
9app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
-
9app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
-
6app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVideoCallEventViewHolder.java
-
23app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
-
15app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java
-
2app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectReactionViewHolder.java
-
2app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/RecipientThreadViewHolder.java
-
6app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java
-
4app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
-
6app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java
-
268app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java
-
80app/src/main/java/awais/instagrabber/asyncs/CreateThreadAction.java
-
42app/src/main/java/awais/instagrabber/asyncs/DownloadedCheckerAsyncTask.java
-
20app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java
-
20app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java
-
160app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java
-
22app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java
-
21app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java
-
17app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
-
2app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java
-
165app/src/main/java/awais/instagrabber/customviews/FormattedNumberTextView.java
-
75app/src/main/java/awais/instagrabber/customviews/FragmentNavigatorWithDefaultAnimations.java
-
246app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java
-
33app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java
-
35app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java
-
60app/src/main/java/awais/instagrabber/customviews/NavHostFragmentWithDefaultAnimations.java
-
42app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
-
6app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java
@ -0,0 +1,5 @@ |
|||
{ |
|||
"JAVA": { |
|||
"TOO_MANY_IVARS": [8, 10, 20, 30] |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
blank_issues_enabled: true |
|||
contact_links: |
|||
- name: Community Chatrooms |
|||
url: https://barinsta.austinhuang.me/en/latest/chat/ |
|||
about: Chat with developers and users alike! |
|||
- name: /r/Barinsta |
|||
url: https://reddit.com/r/barinsta |
|||
about: Start a discussion on our subreddit! |
|||
- name: Repository Discussions |
|||
url: https://github.com/austinhuang0131/barinsta/discussions |
|||
about: Start a discussion in this repo! |
@ -1,16 +0,0 @@ |
|||
--- |
|||
name: General questions & feedback |
|||
about: These should be submitted to either one of our chatrooms, the discussions, or r/barinsta on reddit. |
|||
title: "[Q]" |
|||
labels: question |
|||
assignees: '' |
|||
--- |
|||
|
|||
STOP!!! STOP!!! STOP!!! |
|||
|
|||
General questions & feedback should be submitted to |
|||
* one of our chatrooms (see README.md), or |
|||
* the GitHub discussions section, or |
|||
* r/barinsta on reddit. |
|||
|
|||
STOP!!! STOP!!! STOP!!! |
@ -15,18 +15,19 @@ jobs: |
|||
uses: actions/checkout@v2 |
|||
|
|||
- name: set up JDK 1.8 |
|||
uses: actions/setup-java@v1 |
|||
uses: actions/setup-java@v2 |
|||
with: |
|||
java-version: 1.8 |
|||
|
|||
distribution: 'zulu' |
|||
java-version: '8' |
|||
|
|||
- name: Grant execute permission for gradlew |
|||
run: chmod +x gradlew |
|||
|
|||
- name: Build Github unsigned apk |
|||
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre |
|||
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre --project-prop split |
|||
|
|||
- name: Sign APK |
|||
uses: r0adkll/sign-android-release@v1 |
|||
uses: ammargitham/[email protected].1 |
|||
# ID used to access action output |
|||
id: sign_app |
|||
with: |
|||
@ -45,7 +46,8 @@ jobs: |
|||
uses: actions/upload-artifact@v2 |
|||
with: |
|||
name: barinsta_nightly_${{ steps.date.outputs.date }} |
|||
path: ${{steps.sign_app.outputs.signedReleaseFile}} |
|||
# path: ${{steps.sign_app.outputs.signedReleaseFile}} |
|||
path: app/build/outputs/apk/github/release/*-signed.apk |
|||
|
|||
# Send success notification |
|||
- name: Send success Telegram notification |
|||
@ -55,7 +57,8 @@ jobs: |
|||
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }} |
|||
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }} |
|||
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} completed successfully.\nhttps://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" |
|||
document: ${{steps.sign_app.outputs.signedReleaseFile}} |
|||
# document: ${{steps.sign_app.outputs.signedReleaseFile}} |
|||
document: app/build/outputs/apk/github/release/*-signed.apk |
|||
|
|||
# Send failure notification |
|||
- name: Send failure Telegram notification |
|||
|
@ -14,20 +14,21 @@ jobs: |
|||
steps: |
|||
- name: Checkout |
|||
uses: actions/checkout@v2 |
|||
|
|||
|
|||
- name: set up JDK 1.8 |
|||
uses: actions/setup-java@v1 |
|||
uses: actions/setup-java@v2 |
|||
with: |
|||
java-version: 1.8 |
|||
|
|||
distribution: 'zulu' |
|||
java-version: '8' |
|||
|
|||
- name: Grant execute permission for gradlew |
|||
run: chmod +x gradlew |
|||
|
|||
|
|||
- name: Build Github unsigned pre-release apk |
|||
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre |
|||
|
|||
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre --project-prop split |
|||
|
|||
- name: Sign APK |
|||
uses: r0adkll/sign-android-release@v1 |
|||
uses: ammargitham/[email protected].1 |
|||
# ID used to access action output |
|||
id: sign_app |
|||
with: |
|||
@ -36,18 +37,19 @@ jobs: |
|||
alias: ${{ secrets.ALIAS }} |
|||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} |
|||
keyPassword: ${{ secrets.KEY_PASSWORD }} |
|||
|
|||
|
|||
- name: Get current date and time |
|||
id: date |
|||
run: echo "::set-output name=date::$(date +'%Y%m%d_%H%M%S')" |
|||
|
|||
# Create artifact |
|||
|
|||
# Create artifact |
|||
- name: Create apk artifact |
|||
uses: actions/upload-artifact@v2 |
|||
with: |
|||
name: barinsta_pre-release_${{ steps.date.outputs.date }} |
|||
path: ${{steps.sign_app.outputs.signedReleaseFile}} |
|||
|
|||
# path: ${{steps.sign_app.outputs.signedReleaseFile}} |
|||
path: app/build/outputs/apk/github/release/*-signed.apk |
|||
|
|||
# Send success notification |
|||
- name: Send success Telegram notification |
|||
if: ${{ success() }} |
|||
@ -56,8 +58,9 @@ jobs: |
|||
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }} |
|||
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }} |
|||
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} completed successfully.\nURL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" |
|||
document: ${{steps.sign_app.outputs.signedReleaseFile}} |
|||
|
|||
# document: ${{steps.sign_app.outputs.signedReleaseFile}} |
|||
document: app/build/outputs/apk/github/release/*-signed.apk |
|||
|
|||
# Send failure notification |
|||
- name: Send failure Telegram notification |
|||
if: ${{ failure() }} |
|||
|
@ -1,6 +1,6 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project version="4"> |
|||
<component name="CompilerConfiguration"> |
|||
<bytecodeTargetLevel target="1.8" /> |
|||
<bytecodeTargetLevel target="11" /> |
|||
</component> |
|||
</project> |
@ -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')" |
|||
] |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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<RecentSearch> insertListReversed = ImmutableList |
|||
.<RecentSearch>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<RecentSearch> 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; |
|||
} |
|||
} |
@ -1,6 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
<string name="enable_sentry">Povolit Sentry</string> |
|||
<string name="sentry_summary">Sentry je listener/handler, který zaznamenává chyby a asynchronně je posílá na Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry se spustí při příštím spuštění</string> |
|||
</resources> |
@ -1,6 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
<string name="enable_sentry">Aktiviere Sentry</string> |
|||
<string name="sentry_summary">Sentry ist ein Listener/Handler für Fehler, der den Fehler/das Ereignis asynchron an Sentry.io sendet</string> |
|||
<string name="sentry_start_next_launch">Sentry startet beim nächsten Start</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Sentry 활성화</string> |
|||
<string name="sentry_summary">Sentry는 Sentry.io에 오류를 비동기적으로 보내는 오류 처리기입니다</string> |
|||
<string name="sentry_start_next_launch">Sentry는 다음 출시에 시작됩니다</string> |
|||
</resources> |
@ -1,6 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
<string name="enable_sentry">Sentry inschakelen</string> |
|||
<string name="sentry_summary">Sentry is een luister/handler voor fouten die asynchroon de fout/gebeurtenis versturen naar Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry zal starten bij de volgende lancering</string> |
|||
</resources> |
@ -1,6 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Włącz Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_summary">Sentry jest słuchaczem/obsługą błędów, które asynchronicznie wysyłają błąd/zdarzenie do Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry rozpocznie się przy następnym uruchomieniu</string> |
|||
</resources> |
@ -1,6 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Включить режим \"часового\"</string> |
|||
<string name="sentry_summary">\"Часовой\" - это слушатель/обработчик ошибок, который асинхронно отправляет ошибку/событие на Sentry.io</string> |
|||
<string name="sentry_start_next_launch">\"Часовой\" будет запущен при следующем запуске</string> |
|||
<string name="enable_sentry">Включить Sentry</string> |
|||
<string name="sentry_summary">Sentry - это обработчик событий, который асинхронно отправляет сообщения об ошибках/поломках на Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry включится при следующем запуске приложения</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -1,6 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
<string name="enable_sentry">Bật Sentry</string> |
|||
<string name="sentry_summary">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</string> |
|||
<string name="sentry_start_next_launch">Sentry sẽ được bật vào lần khởi động kế tiếp</string> |
|||
</resources> |
@ -1,6 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
<string name="enable_sentry">啟用 Sentry</string> |
|||
<string name="sentry_summary">Sentry 將會錯誤報告發送至 Sentry.io</string> |
|||
<string name="sentry_start_next_launch">下次啟用應用程式時將會開啟 Sentry</string> |
|||
</resources> |
@ -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<RequestListener> 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()); |
|||
} |
|||
} |
|||
} |
@ -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<RequestListener> 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); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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<ProcessCameraProvider> 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); |
|||
} |
|||
} |
@ -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<String>, 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 |
|||
} |
|||
} |
@ -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<Media>() { |
|||
@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); |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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() |
|||
} |
|||
} |
@ -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<NavController> 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<BottomNavigationView> behavior; |
|||
private List<Tab> currentTabs; |
|||
private List<Integer> 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<BottomNavigationView>) 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<NavBackStackEntry> 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<SearchResponse> 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<SearchResponse> cb = new Callback<SearchResponse>() { |
|||
@Override |
|||
public void onResponse(@NonNull final Call<SearchResponse> call, |
|||
@NonNull final Response<SearchResponse> response) { |
|||
final MatrixCursor cursor; |
|||
final SearchResponse body = response.body(); |
|||
if (body == null) { |
|||
cursor = null; |
|||
return; |
|||
} |
|||
final List<SearchItem> 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<SearchResponse> 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<Integer> 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<NavController> 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<Tab> 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<Tab> 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<Tab> setupMainBottomNav() { |
|||
final Menu menu = binding.bottomNavView.getMenu(); |
|||
menu.clear(); |
|||
final List<Tab> 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<Integer> getMainNavList(final int main_nav_ids) { |
|||
// final TypedArray navIds = getResources().obtainTypedArray(main_nav_ids); |
|||
// final List<Integer> 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<NavBackStackEntry> 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<NavController>() { |
|||
@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<Tab> 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); |
|||
} |
|||
} |
@ -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<NavController>? = 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<BottomNavigationView>? = null |
|||
var currentTabs: List<Tab> = emptyList() |
|||
private set |
|||
private var showBottomViewDestinations: List<Int> = 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<BottomNavigationView> |
|||
// } 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<Int?>? -> |
|||
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<Tab>) { |
|||
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<Tab> { |
|||
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<Tab> { |
|||
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<NavController?> { |
|||
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<Int> = 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 |
|||
} |
|||
} |
@ -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<FavoriteType> categories; |
|||
|
|||
public SearchCategoryAdapter(@NonNull final Fragment fragment, |
|||
@NonNull final List<FavoriteType> 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(); |
|||
} |
|||
} |
@ -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<RecyclerView.ViewHolder> { |
|||
private static final String TAG = SearchItemsAdapter.class.getSimpleName(); |
|||
private static final DiffUtil.ItemCallback<SearchItemOrHeader> DIFF_CALLBACK = new DiffUtil.ItemCallback<SearchItemOrHeader>() { |
|||
@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<SearchItemOrHeader> 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<SearchItem> list) { |
|||
if (list == null) { |
|||
differ.submitList(null); |
|||
return; |
|||
} |
|||
differ.submitList(sectionAndSort(list)); |
|||
} |
|||
|
|||
public void submitList(@Nullable final List<SearchItem> list, @Nullable final Runnable commitCallback) { |
|||
if (list == null) { |
|||
differ.submitList(null, commitCallback); |
|||
return; |
|||
} |
|||
differ.submitList(sectionAndSort(list), commitCallback); |
|||
} |
|||
|
|||
@NonNull |
|||
private List<SearchItemOrHeader> sectionAndSort(@NonNull final List<SearchItem> 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<SearchItem> 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<SearchItemOrHeader> 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); |
|||
} |
|||
} |
|||
} |
@ -1,77 +0,0 @@ |
|||
package awais.instagrabber.adapters; |
|||
|
|||
import android.content.Context; |
|||
import android.database.Cursor; |
|||
import android.util.Log; |
|||
import android.view.LayoutInflater; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.cursoradapter.widget.CursorAdapter; |
|||
|
|||
import awais.instagrabber.databinding.ItemSuggestionBinding; |
|||
import awais.instagrabber.models.enums.SuggestionType; |
|||
|
|||
public final class SuggestionsAdapter extends CursorAdapter { |
|||
private static final String TAG = "SuggestionsAdapter"; |
|||
|
|||
private final OnSuggestionClickListener onSuggestionClickListener; |
|||
|
|||
public SuggestionsAdapter(final Context context, |
|||
final OnSuggestionClickListener onSuggestionClickListener) { |
|||
super(context, null, FLAG_REGISTER_CONTENT_OBSERVER); |
|||
this.onSuggestionClickListener = onSuggestionClickListener; |
|||
} |
|||
|
|||
@Override |
|||
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { |
|||
final LayoutInflater layoutInflater = LayoutInflater.from(context); |
|||
final ItemSuggestionBinding binding = ItemSuggestionBinding.inflate(layoutInflater, parent, false); |
|||
return binding.getRoot(); |
|||
// return layoutInflater.inflate(R.layout.item_suggestion, parent, false); |
|||
} |
|||
|
|||
@Override |
|||
public void bindView(@NonNull final View view, final Context context, @NonNull final Cursor cursor) { |
|||
// i, username, fullname, type, query, picUrl, verified |
|||
// 0, 1 , 2 , 3 , 4 , 5 , 6 |
|||
final String fullName = cursor.getString(2); |
|||
String username = cursor.getString(1); |
|||
String picUrl = cursor.getString(5); |
|||
final boolean verified = cursor.getString(6).charAt(0) == 't'; |
|||
|
|||
final String type = cursor.getString(3); |
|||
SuggestionType suggestionType = null; |
|||
try { |
|||
suggestionType = SuggestionType.valueOf(type); |
|||
} catch (IllegalArgumentException e) { |
|||
Log.e(TAG, "Unknown suggestion type: " + type, e); |
|||
} |
|||
if (suggestionType == null) return; |
|||
String query = cursor.getString(4); |
|||
switch (suggestionType) { |
|||
case TYPE_USER: |
|||
username = '@' + username; |
|||
break; |
|||
case TYPE_HASHTAG: |
|||
username = '#' + username; |
|||
break; |
|||
} |
|||
|
|||
if (onSuggestionClickListener != null) { |
|||
final SuggestionType finalSuggestionType = suggestionType; |
|||
view.setOnClickListener(v -> 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); |
|||
} |
|||
} |
@ -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); |
|||
// }); |
|||
// } |
|||
} |
@ -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); |
|||
} |
|||
} |
|||
//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); |
|||
// } |
|||
//} |
@ -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); |
|||
} |
|||
} |
@ -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)); |
|||
} |
|||
} |
|||
} |
@ -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)); |
|||
} |
|||
} |
|||
} |
@ -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<Void, Void, List<CommentModel>> { |
|||
private static final String TAG = "CommentsFetcher"; |
|||
|
|||
private final String shortCode, endCursor; |
|||
private final FetchListener<List<CommentModel>> fetchListener; |
|||
|
|||
public CommentsFetcher(final String shortCode, final String endCursor, final FetchListener<List<CommentModel>> fetchListener) { |
|||
this.shortCode = shortCode; |
|||
this.endCursor = endCursor; |
|||
this.fetchListener = fetchListener; |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
protected List<CommentModel> 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<CommentModel> commentModels = getParentComments(); |
|||
if (commentModels != null) { |
|||
for (final CommentModel commentModel : commentModels) { |
|||
final List<CommentModel> 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<CommentModel> 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<CommentModel> result) { |
|||
if (fetchListener != null) fetchListener.onResult(result); |
|||
} |
|||
|
|||
@NonNull |
|||
private synchronized List<CommentModel> getChildComments(final String commentId) { |
|||
final List<CommentModel> 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<CommentModel> getParentComments() { |
|||
final List<CommentModel> 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<CommentModel> 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; |
|||
} |
|||
} |
@ -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<Void, Void, Void> { |
|||
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<DirectThread> createThreadRequest = directMessagesService.createThread(Collections.singletonList(userId), null); |
|||
createThreadRequest.enqueue(new Callback<DirectThread>() { |
|||
@Override |
|||
public void onResponse(@NonNull final Call<DirectThread> call, @NonNull final Response<DirectThread> 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<DirectThread> call, @NonNull final Throwable t) { |
|||
onTaskCompleteListener.onTaskComplete(null); |
|||
} |
|||
}); |
|||
return null; |
|||
} |
|||
|
|||
// @Override |
|||
// protected void onPostExecute() { |
|||
// } |
|||
|
|||
public interface OnTaskCompleteListener { |
|||
void onTaskComplete(final DirectThread thread); |
|||
} |
|||
} |
@ -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<Media, Void, Map<String, List<Boolean>>> { |
|||
private static final String TAG = "DownloadedCheckerAsyncTask"; |
|||
|
|||
private final OnCheckResultListener listener; |
|||
|
|||
public DownloadedCheckerAsyncTask(final OnCheckResultListener listener) { |
|||
this.listener = listener; |
|||
} |
|||
|
|||
@Override |
|||
protected Map<String, List<Boolean>> doInBackground(final Media... feedModels) { |
|||
if (feedModels == null) { |
|||
return null; |
|||
} |
|||
final Map<String, List<Boolean>> map = new HashMap<>(); |
|||
for (final Media media : feedModels) { |
|||
map.put(media.getPk(), DownloadUtils.checkDownloaded(media)); |
|||
} |
|||
return map; |
|||
} |
|||
|
|||
@Override |
|||
protected void onPostExecute(final Map<String, List<Boolean>> result) { |
|||
if (listener == null) return; |
|||
listener.onResult(result); |
|||
} |
|||
|
|||
public interface OnCheckResultListener { |
|||
void onResult(final Map<String, List<Boolean>> result); |
|||
} |
|||
} |
@ -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<Void, Void, Media> { |
|||
private static final String TAG = "PostFetcher"; |
|||
|
|||
private final String shortCode; |
|||
private final FetchListener<Media> fetchListener; |
|||
|
|||
public PostFetcher(final String shortCode, final FetchListener<Media> 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<PostChild> 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); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
}); |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
|||
|
@ -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; |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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())); |
|||
} |
|||
} |
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue