Browse Source

init

legacy
Austin Huang 4 years ago
commit
13beabf741
No known key found for this signature in database GPG Key ID: 84C23AA04587A91F
  1. 16
      .gitignore
  2. 54
      .gitlab-ci.yml
  3. 1
      .idea/.name
  4. 119
      .idea/codeStyles
  5. 21
      .idea/gradle.xml
  6. 7
      .idea/inspectionProfiles/profiles_settings.xml
  7. 35
      .idea/jarRepositories.xml
  8. 49
      .idea/misc.xml
  9. 6
      .idea/render.experimental.xml
  10. 12
      .idea/runConfigurations.xml
  11. 53
      .idea/runConfigurations/app.xml
  12. 6
      .idea/vcs.xml
  13. 193
      CHANGELOG
  14. 674
      LICENSE
  15. 5
      README.md
  16. 1
      app/.gitignore
  17. 50
      app/build.gradle
  18. 29
      app/lint.xml
  19. BIN
      app/play_icon.png
  20. 21
      app/proguard-rules.pro
  21. 218
      app/src/main/AndroidManifest.xml
  22. BIN
      app/src/main/ic_launcher-playstore.png
  23. 66
      app/src/main/java/awais/instagrabber/InstaApp.java
  24. 864
      app/src/main/java/awais/instagrabber/MainHelper.java
  25. 11
      app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.java
  26. 146
      app/src/main/java/awais/instagrabber/activities/CommentsViewer.java
  27. 113
      app/src/main/java/awais/instagrabber/activities/DirectMessages.java
  28. 101
      app/src/main/java/awais/instagrabber/activities/DirectMessagesUserInbox.java
  29. 348
      app/src/main/java/awais/instagrabber/activities/FollowViewer.java
  30. 130
      app/src/main/java/awais/instagrabber/activities/Login.java
  31. 480
      app/src/main/java/awais/instagrabber/activities/Main.java
  32. 639
      app/src/main/java/awais/instagrabber/activities/PostViewer.java
  33. 215
      app/src/main/java/awais/instagrabber/activities/ProfileViewer.java
  34. 354
      app/src/main/java/awais/instagrabber/activities/StoryViewer.java
  35. 136
      app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java
  36. 116
      app/src/main/java/awais/instagrabber/adapters/DirectMessagesAdapter.java
  37. 87
      app/src/main/java/awais/instagrabber/adapters/DiscoverAdapter.java
  38. 486
      app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java
  39. 57
      app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java
  40. 144
      app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java
  41. 53
      app/src/main/java/awais/instagrabber/adapters/HighlightsAdapter.java
  42. 354
      app/src/main/java/awais/instagrabber/adapters/MessageItemsAdapter.java
  43. 92
      app/src/main/java/awais/instagrabber/adapters/PostsAdapter.java
  44. 68
      app/src/main/java/awais/instagrabber/adapters/PostsMediaAdapter.java
  45. 75
      app/src/main/java/awais/instagrabber/adapters/SimpleAdapter.java
  46. 84
      app/src/main/java/awais/instagrabber/adapters/StoriesAdapter.java
  47. 60
      app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java
  48. 85
      app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java
  49. 43
      app/src/main/java/awais/instagrabber/adapters/viewholder/DirectMessageViewHolder.java
  50. 22
      app/src/main/java/awais/instagrabber/adapters/viewholder/DiscoverViewHolder.java
  51. 52
      app/src/main/java/awais/instagrabber/adapters/viewholder/FeedItemViewHolder.java
  52. 22
      app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java
  53. 21
      app/src/main/java/awais/instagrabber/adapters/viewholder/HighlightViewHolder.java
  54. 20
      app/src/main/java/awais/instagrabber/adapters/viewholder/PostMediaViewHolder.java
  55. 23
      app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewHolder.java
  56. 91
      app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/TextMessageViewHolder.java
  57. 265
      app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java
  58. 194
      app/src/main/java/awais/instagrabber/asyncs/DiscoverFetcher.java
  59. 248
      app/src/main/java/awais/instagrabber/asyncs/DownloadAsync.java
  60. 194
      app/src/main/java/awais/instagrabber/asyncs/FeedFetcher.java
  61. 103
      app/src/main/java/awais/instagrabber/asyncs/FeedStoriesFetcher.java
  62. 101
      app/src/main/java/awais/instagrabber/asyncs/FollowFetcher.java
  63. 87
      app/src/main/java/awais/instagrabber/asyncs/HighlightsFetcher.java
  64. 146
      app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java
  65. 134
      app/src/main/java/awais/instagrabber/asyncs/PostsFetcher.java
  66. 83
      app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java
  67. 120
      app/src/main/java/awais/instagrabber/asyncs/ProfilePictureFetcher.java
  68. 102
      app/src/main/java/awais/instagrabber/asyncs/StoryStatusFetcher.java
  69. 98
      app/src/main/java/awais/instagrabber/asyncs/SuggestionsFetcher.java
  70. 54
      app/src/main/java/awais/instagrabber/asyncs/UsernameFetcher.java
  71. 100
      app/src/main/java/awais/instagrabber/asyncs/direct_messages/InboxFetcher.java
  72. 76
      app/src/main/java/awais/instagrabber/asyncs/direct_messages/UserInboxFetcher.java
  73. 105
      app/src/main/java/awais/instagrabber/customviews/CircularImageView.java
  74. 17
      app/src/main/java/awais/instagrabber/customviews/CommentMentionClickSpan.java
  75. 25
      app/src/main/java/awais/instagrabber/customviews/FixedImageView.java
  76. 986
      app/src/main/java/awais/instagrabber/customviews/MouseDrawer.java
  77. 179
      app/src/main/java/awais/instagrabber/customviews/RamboTextView.java
  78. 182
      app/src/main/java/awais/instagrabber/customviews/RemixDrawerLayout.java
  79. 37
      app/src/main/java/awais/instagrabber/customviews/helpers/GridAutofitLayoutManager.java
  80. 31
      app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java
  81. 67
      app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoader.java
  82. 34
      app/src/main/java/awais/instagrabber/customviews/helpers/SwipeGestureListener.java
  83. 282
      app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java
  84. 252
      app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/SoundParser.java
  85. 5
      app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveFormProgressChangeListener.java
  86. 7
      app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveGravity.java
  87. 225
      app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveformSeekBar.java
  88. 98
      app/src/main/java/awais/instagrabber/dialogs/AboutDialog.java
  89. 62
      app/src/main/java/awais/instagrabber/dialogs/ProfileSettingsDialog.java
  90. 169
      app/src/main/java/awais/instagrabber/dialogs/QuickAccessDialog.java
  91. 213
      app/src/main/java/awais/instagrabber/dialogs/SettingsDialog.java
  92. 173
      app/src/main/java/awais/instagrabber/dialogs/TimeSettingsDialog.java
  93. 153
      app/src/main/java/awais/instagrabber/directdownload/DirectDownload.java
  94. 117
      app/src/main/java/awais/instagrabber/directdownload/MultiDirectDialog.java
  95. 6
      app/src/main/java/awais/instagrabber/interfaces/FetchListener.java
  96. 11
      app/src/main/java/awais/instagrabber/interfaces/ItemGetter.java
  97. 5
      app/src/main/java/awais/instagrabber/interfaces/LazyLoadListener.java
  98. 7
      app/src/main/java/awais/instagrabber/interfaces/MentionClickListener.java
  99. 5
      app/src/main/java/awais/instagrabber/interfaces/OnGroupClickListener.java
  100. 5
      app/src/main/java/awais/instagrabber/interfaces/SwipeEvent.java

16
.gitignore

@ -0,0 +1,16 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/markdown-navigator.xml
/.idea/markdown-navigator-enh.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx

54
.gitlab-ci.yml

@ -0,0 +1,54 @@
image: openjdk:8-jdk
variables:
ANDROID_COMPILE_SDK: "29"
ANDROID_BUILD_TOOLS: "29.0.2"
ANDROID_SDK_TOOLS: "4333796"
before_script:
# - export vercode=$(cat ./app/build.gradle | grep versionName)
# - export vercode=$(echo $vercode | awk -F[=\'] '{print $2}')
# - echo $vercode > vercode.txt
- apt-get --quiet update --yes
- apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
- if [ -f "android-sdk.zip" ]; then echo "exists!!" ; else wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip ; fi
- unzip -d android-sdk-linux android-sdk.zip
- echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null
- echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null
- echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null
- export ANDROID_HOME=$PWD/android-sdk-linux
- export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/
- chmod +x ./gradlew
- set +o pipefail
- yes | android-sdk-linux/tools/bin/sdkmanager --licenses
- set -o pipefail
stages:
- release
# lintDebug:
# stage: build
# script:
# - ./gradlew -Pci --console=plain :app:lintDebug -PbuildDir=lint
assembleDebug:
stage: release
script:
- ./gradlew assembleDebug
artifacts:
expire_in: 4 days
paths:
- app/build/outputs/
# assembleRelease:
# stage: release
# script:
# - ./gradlew assembleRelease
# -Pandroid.injected.signing.store.file=$(pwd)/.RELEASE.jks
# -Pandroid.injected.signing.store.password=$PSWD
# -Pandroid.injected.signing.key.alias=$AIZA
# -Pandroid.injected.signing.key.password=$PSWD
# artifacts:
# paths:
# - app/build/outputs/

1
.idea/.name

@ -0,0 +1 @@
InstaGrabber

119
.idea/codeStyles

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>
</project>

21
.idea/gradle.xml

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

7
.idea/inspectionProfiles/profiles_settings.xml

@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Default" />
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

35
.idea/jarRepositories.xml

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

49
.idea/misc.xml

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="androidx.annotation.Nullable" />
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="12">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="7" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="11" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="11">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

6
.idea/render.experimental.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="showDecorations" value="true" />
</component>
</project>

12
.idea/runConfigurations.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

53
.idea/runConfigurations/app.xml

@ -0,0 +1,53 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="app" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY_CLASS" value="awais.instagrabber.activities.Main" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

6
.idea/vcs.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

193
CHANGELOG

@ -0,0 +1,193 @@
v15.9
note: there will be no F-Droid updates from this version (v15.9) and onward, download updates from repo's Releases page.
+ added user stories in Feed
+ added frame to currently showing slider item
+ removed tap to pause/resume from Post viewer, replaced with controller
+ fixed swipe not working when posts are opened from stories
+ fixed comments not showing for slider items
v15.8
+ added user's website in profile
+ fixed caption mentions length (@kernoeb)
+ fixed some translations (@kernoeb)
+ fixd feed captions merging with "... more"
v15.4
+ ADDED FRENCH AND SPANISH!!!
+-> Huge thanks to @kernoeb (Telegram) for French translation and @sguinetti (GitLab) for Spanish translation!!
+ added custom post time format support!
+ fixed flickering after changing settings
+ fixed posts not showing after searching from a private profile
+ fixed stories and profile pictures not downloading in user folders even when option was enabled
+ fixed issues with feed, discover and post viewer
+ fixed search suggestions crashes
v15.2
+ fixed feed video not pausing when opened in post viewer
+ added 1 new profile picture view mode
+ added fields in LogCollector to better understand how things went wrong
+ better comments, story, feed, discover and suggestion fetchers
v15.0
+ added support for Instagram.com urls! (:
+ added "Downloaded" check on posts if they've already been downloaded (as per suggestion)
+ fixed highlights scrolling issues
+ fixed comments issues
+ fixed posts weren't showing after searching anything from a private account
+ fixed suggestions not showing sometimes
+ fixed Stories viewer swipe issues
+ fixed Import/Export dialog not showing on old phones
+ added user's name in suggestions search list
+ added comment likes in Comments viewer
+ sending logs won't add empty logs to archive anymore!
+ fixed no new line in logs
+ handled Login WebView lifecycles
+ a better way of handling highlight swipes
+ removed useless code parts
v14.5
+ added changelog after update
+ added swipe support in both Discover/Explore and Feed pages
+ added Send Logs button in Settings to send logs when something doesn't work
+ added clickable user profile in Post Viewer
+ fixed weirdly collapsing toolbar when toolbar is shown at the top
v14.0
+ added theme selection support
+ added import/export settings, favorites and logins functionality (thanks to Airikr [@edgren] on Telegram)
+ added support for downloading slider items from User Feed page
+ added support for usernames and hashtags in user's biography/about text
+ added multiple selection in Discover/Explore page
+ added post date for feed items
+ added some more date formats
+ copyable feed item caption (long tap)
+ fixed late refresh indicator in Followers/Following comparison mode
+ changed feed item size to squares
+ fixed some caption text issues having mentions and hashtags
+ removed clipboard listener (stopped working after some changes)
+ added log collector for different crash scenarios
v13.7
+ fixed custom download folder selection issues
v13.3
+ added discover/explore page (only for logged in users)
+ added function to remove IPTC tracking data from downloaded pictures (thanks to Airikr [@edgren] on Telegram)
+ added multiple accounts support (quick access) and favorites (a suggestion from Saurabh on Telegram)
+ added custom download folder option, you can select where to download posts (a suggestion from Airikr)
+ added desktop mode toggle in Login activity (a suggestion from Eymen on Telegram)
+ added post date in post viewer (a suggestion from W on Telegram)
+ added post time format settings [ Settings > Post Time Settings ]
+ fixed some icons and layouts
+ removed color from slider items in feed
+ removed useless methods and properties
+ better way of handling multiple stories and posts (sliders) in post viewer
+ tried to make notifications grouped together.. hope they work
+ some other fixes and additions which i probably forgot, cause i'm a human ffs
v13.0
+ fixed crash when searching hashtags
+ added lazy loading for hashtags
+ added Show Feed option in Settings (feed may crash app on some phones)
+ added null check in Download async
+ better/adapatable icon
+ fixed sheet dialog themes
v12.7
+ (probably) fixed inflating issue in some devices
v12.5
+ some small performance improvements
v12.0
+ added feed!! (only for logged in users)
+ fixed multiple hashtags with no spaces between them
+ stopped activity from recreating when nothing changed
+ changed highlights to RecyclerView instead of HorizontalScrollView
+ changed some numbers and precisions cause she left me on read
v11.0
+ added crash reporting library
+ added mute/unmute for session
+ better profile picture viewer + profile picture info
+ better swipe gesture
+ fixed mention and hashtag issues
v10.0
NOTE: YOU MAY NEED TO LOGIN TO VIEW PROFILES CAUSE OF INSTAGRAM CHANGES.
+ added direct download multiple posts dialog
+ fixed notification problems
+ fixed some direct download problems
+ fixed batch download and username folder not creating in some activities
+ fixed update checker
v9.0
+ added search in comments viewer
+ added settings to auto or lazy load posts
+ added user & hashtag stack when back pressed
+ added loading icon when loading posts
+ profile info bar sticks to top
+ fixed highlights and profile picture size in portrait and landscape mode
+ fixed posts loading from other user when restarting app
+ fixed posts size changing when scrolled
+ users & hashtag search shows only users or hashtag when first char is @ or #
+ scrolls to top when back pressed
v8.0
+ added pull-to-refresh layout in main posts, followers/following viewer
+ added search in followers/following viewer
+ added video views in post viewer
+ added animation when showing highlights (if available)
+ fixed long usernames not animating (marquee)
+ fixed padding and size in portrait and landscape modes
+ fixed accounts showing personal followers/following when account has 0 followers/following
+ fixed double ripple when tapped on profile picture
+ smaller story border around profile picture
v7.0
+ added comments viewer!!
+ added highest quality post fetcher
+ added loading indicator where it was missing before
+ fixed highlight name alignment
+ fixed swiping on posts opened via Share or link
v6.0
+ added story highlights!! (issue #5)
+ added button to view posts posted in stories
+ added verified badge for accounts
+ fixed posts & story swiping issues
+ fixed slow loading and stuff
+ fixed different margins and sizes
+ fixed activity not recreating after settings dialogs closed
v5.0
+ added followers / following checker
+ added search view suggestions for usernames & hashtags
+ added sliding profile container
+ fixed batch download permission issue (issue #4)
+ fixed some small screen panning issues
+ fixed search view width
+ fixed update checker
v4.0
+ fixed Login and Visit project page button codes.
v3.0
+ fixed posts merged from different accounts when searched while posts are loading!
+ view stories (only if you're logged in)
+ directly download posts
+ choose between two pfp (profile picture) viewer methods
+ fixed search box not showing up when toolbar is at bottom
+ automatically checks for updates
v2.0
+ fixed Login crashes
v1.0
+ first ever changelog
+ basic stuff like downloading profile pics, posts and copying captions and bio.
+ batch download posts

674
LICENSE

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
InstaGrabber
Copyright (C) 2019 AWAiS
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
InstaGrabber Copyright (C) 2019 AWAiS
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

5
README.md

@ -0,0 +1,5 @@
![InstaGrabber](./app/src/main/res/mipmap-hdpi/ic_launcher.png "InstaGrabber") InstaGrabber
Revived Version by Austin.
Coming soon™

1
app/.gitignore

@ -0,0 +1 @@
/build

50
app/build.gradle

@ -0,0 +1,50 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
defaultConfig {
applicationId 'awais.instagrabber'
minSdkVersion 16
targetSdkVersion 29
versionCode 27
versionName '16.5-a1'
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
vectorDrawables.generatedDensities = []
}
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
buildFeatures { viewBinding true }
aaptOptions { additionalParameters '--no-version-vectors' }
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation('androidx.appcompat:appcompat:1.3.0-alpha01@aar') { transitive true }
implementation('androidx.recyclerview:recyclerview:1.2.0-alpha03@aar') { transitive true }
implementation('com.google.android.material:material:1.3.0-alpha01@aar') { transitive true }
implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01') { transitive true }
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
implementation('org.jsoup:jsoup:1.13.1') { transitive true }
implementation('com.github.bumptech.glide:glide:4.11.0') { transitive true }
implementation('com.github.chrisbanes:PhotoView:v2.0.0@aar') { transitive true }
implementation('com.google.android.exoplayer:exoplayer:2.11.1@aar') { transitive true }
}

29
app/lint.xml

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="IconColors">
<ignore path="src/main/res/drawable-night/download.png" />
</issue>
<issue id="IconLocation">
<ignore path="src/main/res/drawable/adm.png" />
<ignore path="src/main/res/drawable/check.png" />
<ignore path="src/main/res/drawable/collapse.png" />
<ignore path="src/main/res/drawable/comments.png" />
<ignore path="src/main/res/drawable/download.png" />
<ignore path="src/main/res/drawable/expand.png" />
<ignore path="src/main/res/drawable/lock.png" />
<ignore path="src/main/res/drawable/lw.png" />
<ignore path="src/main/res/drawable/ms.png" />
<ignore path="src/main/res/drawable/mute.png" />
<ignore path="src/main/res/drawable/qdb.png" />
<ignore path="src/main/res/drawable/rev.png" />
<ignore path="src/main/res/drawable/revl.png" />
<ignore path="src/main/res/drawable/settings.png" />
<ignore path="src/main/res/drawable/slider.png" />
<ignore path="src/main/res/drawable/tesv.png" />
<ignore path="src/main/res/drawable/vdz.png" />
<ignore path="src/main/res/drawable/verified.png" />
<ignore path="src/main/res/drawable/video.png" />
<ignore path="src/main/res/drawable/video_views.png" />
<ignore path="src/main/res/drawable/vol.png" />
</issue>
</lint>

BIN
app/play_icon.png

After

Width: 512  |  Height: 512  |  Size: 142 KiB

21
app/proguard-rules.pro

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

218
app/src/main/AndroidManifest.xml

@ -0,0 +1,218 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="awais.instagrabber">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name=".InstaApp"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
<activity
android:name=".activities.Main"
android:launchMode="singleTop"
android:taskAffinity=".Main"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEARCH" />
<action android:name="android.intent.action.WEB_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="ig.me" />
<data android:host="www.ig.me" />
<data android:host="instagram.com" />
<data android:host="www.instagram.com" />
<data android:pathPrefix="/" />
<data android:pathPrefix="/p" />
<data android:pathPrefix="/explore/tags" />
</intent-filter>
</activity>
<activity
android:name="awaisomereport.ErrorReporterActivity"
android:allowEmbedded="false"
android:allowTaskReparenting="false"
android:autoRemoveFromRecents="true"
android:clearTaskOnLaunch="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:documentLaunchMode="never"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true"
android:launchMode="singleTask"
android:lockTaskMode="never"
android:noHistory="false"
android:screenOrientation="portrait"
android:taskAffinity="awais.instagrabber.errorreport"
android:theme="@android:style/Theme.DeviceDefault.Dialog" />
<activity
android:name=".directdownload.MultiDirectDialog"
android:allowEmbedded="false"
android:allowTaskReparenting="false"
android:autoRemoveFromRecents="true"
android:clearTaskOnLaunch="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:documentLaunchMode="never"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true"
android:launchMode="singleTask"
android:lockTaskMode="never"
android:noHistory="false"
android:taskAffinity="awais.instagrabber.multidialog"
android:theme="@style/FlyingGayDialog" />
<activity
android:name=".directdownload.DirectDownload"
android:allowEmbedded="false"
android:allowTaskReparenting="false"
android:autoRemoveFromRecents="true"
android:clearTaskOnLaunch="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:description="@string/direct_download_desc"
android:documentLaunchMode="never"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true"
android:label="@string/direct_download"
android:launchMode="singleTask"
android:lockTaskMode="never"
android:noHistory="false"
android:theme="@style/CompletelyTransparent">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEARCH" />
<action android:name="android.intent.action.WEB_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="ig.me" />
<data android:scheme="http" />
<data android:scheme="https" />
</intent-filter>
</activity>
<activity
android:name=".activities.PostViewer"
android:parentActivityName=".activities.Main">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.Main" />
</activity>
<activity
android:name=".activities.CommentsViewer"
android:parentActivityName=".activities.PostViewer">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.PostViewer" />
</activity>
<activity
android:name=".activities.StoryViewer"
android:parentActivityName=".activities.Main">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.Main" />
</activity>
<activity
android:name=".activities.FollowViewer"
android:parentActivityName=".activities.Main">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.Main" />
</activity>
<activity
android:name=".activities.ProfileViewer"
android:parentActivityName=".activities.Main">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.Main" />
</activity>
<activity
android:name=".activities.Login"
android:label="@string/login"
android:parentActivityName=".activities.Main">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.Main" />
</activity>
<activity
android:name=".activities.DirectMessages"
android:parentActivityName=".activities.Main">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.Main" />
</activity>
<activity
android:name=".activities.DirectMessagesUserInbox"
android:parentActivityName=".activities.DirectMessages">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.DirectMessages" />
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>

BIN
app/src/main/ic_launcher-playstore.png

After

Width: 512  |  Height: 512  |  Size: 70 KiB

66
app/src/main/java/awais/instagrabber/InstaApp.java

@ -0,0 +1,66 @@
package awais.instagrabber;
import android.content.ClipboardManager;
import android.content.Context;
import androidx.core.app.NotificationManagerCompat;
import androidx.multidex.MultiDexApplication;
import java.net.CookieHandler;
import java.text.SimpleDateFormat;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DataBox;
import awais.instagrabber.utils.LocaleUtils;
import awais.instagrabber.utils.SettingsHelper;
import awaisomereport.CrashReporter;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Utils.NET_COOKIE_MANAGER;
import static awais.instagrabber.utils.Utils.changeTheme;
import static awais.instagrabber.utils.Utils.clipboardManager;
import static awais.instagrabber.utils.Utils.dataBox;
import static awais.instagrabber.utils.Utils.datetimeParser;
import static awais.instagrabber.utils.Utils.getInstalledTelegramPackage;
import static awais.instagrabber.utils.Utils.isInstaInstalled;
import static awais.instagrabber.utils.Utils.isInstagramInstalled;
import static awais.instagrabber.utils.Utils.logCollector;
import static awais.instagrabber.utils.Utils.notificationManager;
import static awais.instagrabber.utils.Utils.settingsHelper;
import static awais.instagrabber.utils.Utils.telegramPackage;
public final class InstaApp extends MultiDexApplication {
@Override
public void onCreate() {
super.onCreate();
if (!BuildConfig.DEBUG) CrashReporter.get(this).start();
logCollector = new LogCollector(this);
CookieHandler.setDefault(NET_COOKIE_MANAGER);
final Context appContext = getApplicationContext();
isInstagramInstalled = isInstaInstalled(appContext);
telegramPackage = getInstalledTelegramPackage(appContext);
if (dataBox == null)
dataBox = DataBox.getInstance(appContext);
if (settingsHelper == null)
settingsHelper = new SettingsHelper(this);
LocaleUtils.setLocale(getBaseContext());
if (notificationManager == null)
notificationManager = NotificationManagerCompat.from(appContext);
if (clipboardManager == null)
clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
if (datetimeParser == null)
datetimeParser = new SimpleDateFormat(settingsHelper.getString(Constants.DATE_TIME_FORMAT), LocaleUtils.getCurrentLocale());
changeTheme();
}
}

864
app/src/main/java/awais/instagrabber/MainHelper.java

@ -0,0 +1,864 @@
package awais.instagrabber;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.text.SpannableStringBuilder;
import android.text.method.LinkMovementMethod;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.core.widget.ImageViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.shape.MaterialShapeDrawable;
import java.util.Arrays;
import awais.instagrabber.activities.FollowViewer;
import awais.instagrabber.activities.Main;
import awais.instagrabber.activities.PostViewer;
import awais.instagrabber.activities.StoryViewer;
import awais.instagrabber.adapters.DiscoverAdapter;
import awais.instagrabber.adapters.FeedAdapter;
import awais.instagrabber.adapters.FeedStoriesAdapter;
import awais.instagrabber.adapters.PostsAdapter;
import awais.instagrabber.asyncs.DiscoverFetcher;
import awais.instagrabber.asyncs.FeedFetcher;
import awais.instagrabber.asyncs.FeedStoriesFetcher;
import awais.instagrabber.asyncs.HighlightsFetcher;
import awais.instagrabber.asyncs.PostsFetcher;
import awais.instagrabber.asyncs.ProfileFetcher;
import awais.instagrabber.asyncs.StoryStatusFetcher;
import awais.instagrabber.customviews.MouseDrawer;
import awais.instagrabber.customviews.RamboTextView;
import awais.instagrabber.customviews.helpers.GridAutofitLayoutManager;
import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader;
import awais.instagrabber.customviews.helpers.VideoAwareRecyclerScroller;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.interfaces.MentionClickListener;
import awais.instagrabber.models.BasePostModel;
import awais.instagrabber.models.DiscoverItemModel;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.FeedStoryModel;
import awais.instagrabber.models.IntentModel;
import awais.instagrabber.models.PostModel;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.enums.IntentModelType;
import awais.instagrabber.models.enums.ItemGetType;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Constants.AUTOLOAD_POSTS;
import static awais.instagrabber.utils.Constants.BOTTOM_TOOLBAR;
import static awais.instagrabber.utils.Utils.logCollector;
public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener {
private static AsyncTask<?, ?, ?> currentlyExecuting;
private AsyncTask<Void, Void, FeedStoryModel[]> prevStoriesFetcher;
private final boolean autoloadPosts;
private boolean hasNextPage = false, feedHasNextPage = false, discoverHasMore = false;
private String endCursor = null, feedEndCursor = null, discoverEndMaxId = null;
private final FetchListener<PostModel[]> postsFetchListener = new FetchListener<PostModel[]>() {
@Override
public void onResult(final PostModel[] result) {
if (result != null) {
final int oldSize = main.allItems.size();
main.allItems.addAll(Arrays.asList(result));
postsAdapter.notifyItemRangeInserted(oldSize, result.length);
main.mainBinding.mainPosts.post(() -> {
main.mainBinding.mainPosts.setNestedScrollingEnabled(true);
main.mainBinding.mainPosts.setVisibility(View.VISIBLE);
});
final String username;
final String postFix;
if (!isHashtag) {
username = main.profileModel.getUsername();
postFix = "/" + main.profileModel.getPostCount() + ')';
} else {
username = null;
postFix = null;
}
if (isHashtag)
main.mainBinding.toolbar.toolbar.setTitle(main.getString(R.string.title_hashtag_prefix) + main.userQuery);
else main.mainBinding.toolbar.toolbar.setTitle(username + " (" + main.allItems.size() + postFix);
final PostModel model = result[result.length - 1];
if (model != null) {
endCursor = model.getEndCursor();
if (endCursor == null && !isHashtag) {
main.mainBinding.toolbar.toolbar.setTitle(username + " (" + main.profileModel.getPostCount() + postFix);
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
main.mainBinding.toolbar.toolbar.setTitle(username);
handler.removeCallbacks(this);
}
}, 1000);
}
hasNextPage = model.hasNextPage();
if ((autoloadPosts && hasNextPage) && !isHashtag)
currentlyExecuting = new PostsFetcher(main.profileModel.getId(), endCursor, this)
.setUsername(main.profileModel.getUsername()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else
main.mainBinding.swipeRefreshLayout.setRefreshing(false);
model.setPageCursor(false, null);
}
}
}
};
private final FetchListener<FeedModel[]> feedFetchListener = new FetchListener<FeedModel[]>() {
@Override
public void doBefore() {
main.mainBinding.feedSwipeRefreshLayout.post(() -> main.mainBinding.feedSwipeRefreshLayout.setRefreshing(true));
}
@Override
public void onResult(final FeedModel[] result) {
if (result != null) {
final int oldSize = main.feedItems.size();
main.feedItems.addAll(Arrays.asList(result));
feedAdapter.notifyItemRangeInserted(oldSize, result.length);
main.mainBinding.feedPosts.post(() -> main.mainBinding.feedPosts.setNestedScrollingEnabled(true));
final PostModel feedPostModel = result[result.length - 1];
if (feedPostModel != null) {
feedEndCursor = feedPostModel.getEndCursor();
feedHasNextPage = feedPostModel.hasNextPage();
feedPostModel.setPageCursor(false, null);
}
}
main.mainBinding.feedSwipeRefreshLayout.setRefreshing(false);
}
};
private final FetchListener<DiscoverItemModel[]> discoverFetchListener = new FetchListener<DiscoverItemModel[]>() {
@Override
public void doBefore() {
main.mainBinding.discoverSwipeRefreshLayout.setRefreshing(true);
}
@Override
public void onResult(final DiscoverItemModel[] result) {
if (result != null) {
final int oldSize = main.discoverItems.size();
main.discoverItems.addAll(Arrays.asList(result));
discoverAdapter.notifyItemRangeInserted(oldSize, result.length);
final DiscoverItemModel discoverItemModel = result[result.length - 1];
if (discoverItemModel != null) {
discoverEndMaxId = discoverItemModel.getNextMaxId();
discoverHasMore = discoverItemModel.hasMore();
discoverItemModel.setMore(false, null);
}
}
main.mainBinding.discoverSwipeRefreshLayout.setRefreshing(false);
}
};
private final FetchListener<FeedStoryModel[]> feedStoriesListener = new FetchListener<FeedStoryModel[]>() {
@Override
public void doBefore() {
main.mainBinding.feedStories.setVisibility(View.GONE);
}
@Override
public void onResult(final FeedStoryModel[] result) {
feedStoriesAdapter.setData(result);
if (result != null && result.length > 0)
main.mainBinding.feedStories.setVisibility(View.VISIBLE);
}
};
private final MentionClickListener mentionClickListener = new MentionClickListener() {
@Override
public void onClick(final RamboTextView view, final String text, final boolean isHashtag) {
new AlertDialog.Builder(main).setMessage(isHashtag ? R.string.comment_view_mention_hash_search : R.string.comment_view_mention_user_search)
.setTitle(text).setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok, (dialog, which) -> {
if (Main.scanHack != null) Main.scanHack.onResult(text);
}).show();
}
};
private final FeedStoriesAdapter feedStoriesAdapter = new FeedStoriesAdapter(null, new View.OnClickListener() {
@Override
public void onClick(final View v) {
final Object tag = v.getTag();
if (tag instanceof FeedStoryModel) {
final FeedStoryModel feedStoryModel = (FeedStoryModel) tag;
final StoryModel[] storyModels = feedStoryModel.getStoryModels();
main.startActivity(new Intent(main, StoryViewer.class)
.putExtra(Constants.EXTRAS_STORIES, storyModels)
.putExtra(Constants.EXTRAS_USERNAME, feedStoryModel.getProfileModel().getUsername())
);
}
}
});
@NonNull
private final Main main;
private final Resources resources;
private final View collapsingToolbar;
private final RecyclerLazyLoader lazyLoader;
private boolean isHashtag;
private PostsAdapter postsAdapter;
private FeedAdapter feedAdapter;
private RecyclerLazyLoader feedLazyLoader, discoverLazyLoader;
private DiscoverAdapter discoverAdapter;
public SimpleExoPlayer currentFeedPlayer; // hack for remix drawer layout
public MainHelper(@NonNull final Main main) {
stopCurrentExecutor();
this.main = main;
this.resources = main.getResources();
this.autoloadPosts = Utils.settingsHelper.getBoolean(AUTOLOAD_POSTS);
main.mainBinding.swipeRefreshLayout.setOnRefreshListener(this);
main.mainBinding.mainUrl.setMovementMethod(new LinkMovementMethod());
final boolean isLoggedIn = !Utils.isEmpty(Utils.settingsHelper.getString(Constants.COOKIE));
final LinearLayout iconSlider = main.findViewById(R.id.iconSlider);
final ImageView iconFeed = (ImageView) iconSlider.getChildAt(0);
final ImageView iconProfile = (ImageView) iconSlider.getChildAt(1);
final ImageView iconDiscover = (ImageView) iconSlider.getChildAt(2);
final boolean isBottomToolbar = Utils.settingsHelper.getBoolean(BOTTOM_TOOLBAR);
if (!isLoggedIn) {
main.mainBinding.drawerLayout.removeView(main.mainBinding.feedLayout);
main.mainBinding.drawerLayout.removeView(main.mainBinding.discoverSwipeRefreshLayout);
iconFeed.setAlpha(0.4f);
iconDiscover.setAlpha(0.4f);
} else {
iconFeed.setAlpha(1f);
iconDiscover.setAlpha(1f);
setupExplore();
final boolean showFeed = Utils.settingsHelper.getBoolean(Constants.SHOW_FEED);
if (showFeed) setupFeed();
else {
iconFeed.setAlpha(0.4f);
main.mainBinding.drawerLayout.removeView(main.mainBinding.feedLayout);
}
final TypedValue resolvedAttr = new TypedValue();
main.getTheme().resolveAttribute(android.R.attr.textColorPrimary, resolvedAttr, true);
final int selectedItem = ContextCompat.getColor(main, resolvedAttr.resourceId != 0 ? resolvedAttr.resourceId : resolvedAttr.data);
final ColorStateList colorStateList = ColorStateList.valueOf(selectedItem);
main.mainBinding.toolbar.toolbar.measure(0, -1);
final int toolbarMeasuredHeight = main.mainBinding.toolbar.toolbar.getMeasuredHeight();
final ViewGroup.LayoutParams layoutParams = main.mainBinding.toolbar.toolbar.getLayoutParams();
final MouseDrawer.DrawerListener simpleDrawerListener = new MouseDrawer.DrawerListener() {
private final String titleDiscover = resources.getString(R.string.title_discover);
@Override
public void onDrawerSlide(final View drawerView, @MouseDrawer.EdgeGravity final int gravity, final float slideOffset) {
final int currentIconAlpha = (int) Math.max(100, 255 - 255 * slideOffset);
final int otherIconAlpha = (int) Math.max(100, 255 * slideOffset);
ImageViewCompat.setImageTintList(iconProfile, colorStateList.withAlpha(currentIconAlpha));
final boolean drawerOpening = slideOffset > 0.0f;
final int alpha;
final ColorStateList imageTintList;
if (gravity == GravityCompat.START) {
// this helps hide the toolbar when opening feed
final int roundedToolbarHeight;
final float toolbarHeight;
if (isBottomToolbar) {
toolbarHeight = toolbarMeasuredHeight * slideOffset;
roundedToolbarHeight = -Math.round(toolbarHeight);
} else {
toolbarHeight = -toolbarMeasuredHeight * slideOffset;
roundedToolbarHeight = Math.round(toolbarHeight);
}
layoutParams.height = Math.max(0, Math.min(toolbarMeasuredHeight, toolbarMeasuredHeight + roundedToolbarHeight));
main.mainBinding.toolbar.toolbar.setLayoutParams(layoutParams);
main.mainBinding.toolbar.toolbar.setTranslationY(toolbarHeight);
imageTintList = ImageViewCompat.getImageTintList(iconDiscover);
alpha = imageTintList != null ? (imageTintList.getDefaultColor() & 0xFF_000000) >> 24 : 0;
if (drawerOpening && alpha > 100)
ImageViewCompat.setImageTintList(iconDiscover, colorStateList.withAlpha(currentIconAlpha));
if (showFeed) ImageViewCompat.setImageTintList(iconFeed, colorStateList.withAlpha(otherIconAlpha));
} else {
// this changes toolbar title
main.mainBinding.toolbar.toolbar.setTitle(slideOffset >= 0.466 ? titleDiscover : main.userQuery);
if (showFeed) {
imageTintList = ImageViewCompat.getImageTintList(iconFeed);
alpha = imageTintList != null ? (imageTintList.getDefaultColor() & 0xFF_000000) >> 24 : 0;
if (drawerOpening && alpha > 100)
ImageViewCompat.setImageTintList(iconFeed, colorStateList.withAlpha(currentIconAlpha));
}
ImageViewCompat.setImageTintList(iconDiscover, colorStateList.withAlpha(otherIconAlpha));
}
}
@Override
public void onDrawerOpened(@NonNull final View drawerView, @MouseDrawer.EdgeGravity final int gravity) {
if (gravity == GravityCompat.START || drawerView == main.mainBinding.feedLayout) {
if (currentFeedPlayer != null) {
currentFeedPlayer.setPlayWhenReady(true);
currentFeedPlayer.getPlaybackState();
}
} else {
// clear selection
isSelectionCleared();
}
}
@Override
public void onDrawerClosed(@NonNull final View drawerView, @MouseDrawer.EdgeGravity final int gravity) {
if (gravity == GravityCompat.START || drawerView == main.mainBinding.feedLayout) {
if (currentFeedPlayer != null) {
currentFeedPlayer.setPlayWhenReady(false);
currentFeedPlayer.getPlaybackState();
}
} else {
// clear selection
isSelectionCleared();
}
}
};
ImageViewCompat.setImageTintList(iconFeed, colorStateList.withAlpha(100)); // to change colors when created
ImageViewCompat.setImageTintList(iconProfile, colorStateList.withAlpha(255)); // to change colors when created
ImageViewCompat.setImageTintList(iconDiscover, colorStateList.withAlpha(100)); // to change colors when created
main.mainBinding.drawerLayout.addDrawerListener(simpleDrawerListener);
}
collapsingToolbar = main.mainBinding.appBarLayout.getChildAt(0);
main.mainBinding.mainPosts.setNestedScrollingEnabled(false);
main.mainBinding.highlightsList.setLayoutManager(new LinearLayoutManager(main, LinearLayoutManager.HORIZONTAL, false));
main.mainBinding.highlightsList.setAdapter(main.highlightsAdapter);
int color = -1;
final Drawable background = main.mainBinding.appBarLayout.getBackground();
if (background instanceof MaterialShapeDrawable) {
final MaterialShapeDrawable drawable = (MaterialShapeDrawable) background;
final ColorStateList fillColor = drawable.getFillColor();
if (fillColor != null) color = fillColor.getDefaultColor();
} else {
final Bitmap bitmap = Bitmap.createBitmap(9, 9, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas();
canvas.setBitmap(bitmap);
background.draw(canvas);
color = bitmap.getPixel(4, 4);
if (!bitmap.isRecycled()) bitmap.recycle();
}
if (color == -1 || color == 0) color = resources.getBoolean(R.bool.isNight) ? 0xff212121 : 0xfff5f5f5;
main.mainBinding.profileInfo.setBackgroundColor(color);
main.mainBinding.profileInfo.setClickable(true);
if (!isBottomToolbar) main.mainBinding.toolbar.toolbar.setBackgroundColor(color);
main.mainBinding.appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
private int height;
@Override
public void onOffsetChanged(final AppBarLayout appBarLayout, final int verticalOffset) {
if (height == 0) {
height = main.mainBinding.profileInfo.getHeight();
collapsingToolbar.setMinimumHeight(height);
}
main.mainBinding.profileInfo.setTranslationY(-Math.min(0, verticalOffset));
}
});
main.setSupportActionBar(main.mainBinding.toolbar.toolbar);
if (isBottomToolbar) {
final LinearLayout linearLayout = (LinearLayout) main.mainBinding.toolbar.toolbar.getParent();
linearLayout.removeView(main.mainBinding.toolbar.toolbar);
linearLayout.addView(main.mainBinding.toolbar.toolbar, linearLayout.getChildCount());
}
final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(main, Utils.convertDpToPx(130));
main.mainBinding.mainPosts.setLayoutManager(layoutManager);
main.mainBinding.mainPosts.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4)));
main.mainBinding.mainPosts.setAdapter(postsAdapter = new PostsAdapter(main.allItems, v -> {
final Object tag = v.getTag();
if (tag instanceof PostModel) {
final PostModel postModel = (PostModel) tag;
if (postsAdapter.isSelecting) toggleSelection(postModel);
else main.startActivity(new Intent(main, PostViewer.class)
.putExtra(Constants.EXTRAS_INDEX, postModel.getPosition())
.putExtra(Constants.EXTRAS_POST, postModel)
.putExtra(Constants.EXTRAS_USER, main.userQuery)
.putExtra(Constants.EXTRAS_TYPE, ItemGetType.MAIN_ITEMS));
}
}, v -> { // long click listener
final Object tag = v.getTag();
if (tag instanceof PostModel) {
postsAdapter.isSelecting = true;
toggleSelection((PostModel) tag);
}
return true;
}));
this.lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
if ((!autoloadPosts || isHashtag) && hasNextPage) {
main.mainBinding.swipeRefreshLayout.setRefreshing(true);
stopCurrentExecutor();
currentlyExecuting = new PostsFetcher(isHashtag ? main.userQuery : main.profileModel.getId(), endCursor, postsFetchListener)
.setUsername(isHashtag ? null : main.profileModel.getUsername())
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
endCursor = null;
}
});
main.mainBinding.mainPosts.addOnScrollListener(lazyLoader);
}
private void setupFeed() {
main.mainBinding.feedStories.setLayoutManager(new LinearLayoutManager(main, LinearLayoutManager.HORIZONTAL, false));
main.mainBinding.feedStories.setAdapter(feedStoriesAdapter);
refreshFeedStories();
final LinearLayoutManager layoutManager = new LinearLayoutManager(main);
main.mainBinding.feedPosts.setLayoutManager(layoutManager);
main.mainBinding.feedPosts.setAdapter(feedAdapter = new FeedAdapter(main, main.feedItems, (view, text, isHashtag) ->
new AlertDialog.Builder(main).setMessage(isHashtag ? R.string.comment_view_mention_hash_search : R.string.comment_view_mention_user_search)
.setTitle(text).setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok, (dialog, which) -> {
if (Main.scanHack != null) {
main.mainBinding.drawerLayout.closeDrawers();
Main.scanHack.onResult(text);
}
}).show()));
main.mainBinding.feedSwipeRefreshLayout.setOnRefreshListener(() -> {
refreshFeedStories();
if (feedLazyLoader != null) feedLazyLoader.resetState();
main.feedItems.clear();
if (feedAdapter != null) feedAdapter.notifyDataSetChanged();
new FeedFetcher(feedFetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
main.mainBinding.feedPosts.addOnScrollListener(feedLazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
if (feedHasNextPage) {
main.mainBinding.feedSwipeRefreshLayout.setRefreshing(true);
new FeedFetcher(feedEndCursor, feedFetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
feedEndCursor = null;
}
}));
main.mainBinding.feedPosts.addOnScrollListener(new VideoAwareRecyclerScroller(main, main.feedItems,
(itemPos, player) -> currentFeedPlayer = player));
new FeedFetcher(feedFetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void refreshFeedStories() {
// todo setup feed stories
if (prevStoriesFetcher != null) {
try {
prevStoriesFetcher.cancel(true);
} catch (final Exception e) {
// ignore
}
}
prevStoriesFetcher = new FeedStoriesFetcher(feedStoriesListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void setupExplore() {
final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(main, Utils.convertDpToPx(130));
main.mainBinding.discoverPosts.setLayoutManager(layoutManager);
main.mainBinding.discoverPosts.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4)));
main.mainBinding.discoverSwipeRefreshLayout.setOnRefreshListener(() -> {
if (discoverLazyLoader != null) discoverLazyLoader.resetState();
main.discoverItems.clear();
if (discoverAdapter != null) discoverAdapter.notifyDataSetChanged();
new DiscoverFetcher(null, discoverFetchListener, false).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
main.mainBinding.discoverPosts.setAdapter(discoverAdapter = new DiscoverAdapter(main.discoverItems, v -> {
final Object tag = v.getTag();
if (tag instanceof DiscoverItemModel) {
final DiscoverItemModel itemModel = (DiscoverItemModel) tag;
if (discoverAdapter.isSelecting) toggleDiscoverSelection(itemModel);
else main.startActivity(new Intent(main, PostViewer.class)
.putExtra(Constants.EXTRAS_INDEX, itemModel.getPosition())
.putExtra(Constants.EXTRAS_TYPE, ItemGetType.DISCOVER_ITEMS)
.putExtra(Constants.EXTRAS_POST, new PostModel(itemModel.getShortCode())));
}
}, v -> {
final Object tag = v.getTag();
if (tag instanceof DiscoverItemModel) {
discoverAdapter.isSelecting = true;
toggleDiscoverSelection((DiscoverItemModel) tag);
}
return true;
}));
main.mainBinding.discoverPosts.addOnScrollListener(discoverLazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
if (discoverHasMore) {
main.mainBinding.discoverSwipeRefreshLayout.setRefreshing(true);
new DiscoverFetcher(discoverEndMaxId, discoverFetchListener, false).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
discoverEndMaxId = null;
}
}));
new DiscoverFetcher(null, discoverFetchListener, true).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void onIntent(final Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (!Utils.isEmpty(action) && !Intent.ACTION_MAIN.equals(action)) {
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
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 && !Utils.isEmpty(data)) {
if (data.indexOf('\n') > 0) data = data.substring(data.lastIndexOf('\n') + 1);
final IntentModel model = Utils.stripString(data);
if (model != null) {
final String modelText = model.getText();
final IntentModelType modelType = model.getType();
if (modelType == IntentModelType.POST) {
main.startActivityForResult(new Intent(main, PostViewer.class)
.putExtra(Constants.EXTRAS_USER, main.userQuery)
.putExtra(Constants.EXTRAS_POST, new PostModel(modelText)), 9629);
} else {
main.addToStack();
main.userQuery = modelType == IntentModelType.HASHTAG ? '#' + modelText : modelText;
onRefresh();
}
}
}
}
}
}
@Override
public void onRefresh() {
main.mainBinding.drawerLayout.closeDrawers();
if (lazyLoader != null) lazyLoader.resetState();
stopCurrentExecutor();
main.allItems.clear();
main.selectedItems.clear();
if (postsAdapter != null) {
postsAdapter.isSelecting = false;
postsAdapter.notifyDataSetChanged();
}
main.mainBinding.appBarLayout.setExpanded(true, true);
main.mainBinding.privatePage.setVisibility(View.GONE);
main.mainBinding.mainProfileImage.setImageBitmap(null);
main.mainBinding.mainProfileImage.setImageDrawable(null);
main.mainBinding.mainUrl.setText(null);
main.mainBinding.mainFullName.setText(null);
main.mainBinding.mainPostCount.setText(null);
main.mainBinding.mainFollowers.setText(null);
main.mainBinding.mainFollowing.setText(null);
main.mainBinding.mainBiography.setText(null);
main.mainBinding.mainBiography.setEnabled(false);
main.mainBinding.mainProfileImage.setEnabled(false);
main.mainBinding.mainBiography.setMentionClickListener(null);
main.mainBinding.mainUrl.setVisibility(View.GONE);
main.mainBinding.isVerified.setVisibility(View.GONE);
main.mainBinding.mainPosts.setNestedScrollingEnabled(false);
main.mainBinding.highlightsList.setVisibility(View.GONE);
collapsingToolbar.setVisibility(View.GONE);
main.highlightsAdapter.setData(null);
main.mainBinding.swipeRefreshLayout.setRefreshing(main.userQuery != null);
if (main.userQuery == null) {
main.mainBinding.toolbar.toolbar.setTitle(R.string.app_name);
return;
}
isHashtag = main.userQuery.charAt(0) == '#';
collapsingToolbar.setVisibility(isHashtag ? View.GONE : View.VISIBLE);
if (isHashtag) {
main.mainBinding.toolbar.toolbar.setTitle(resources.getString(R.string.title_hashtag_prefix) + main.userQuery);
main.mainBinding.infoContainer.setVisibility(View.GONE);
currentlyExecuting = new PostsFetcher(main.userQuery, postsFetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
main.mainBinding.toolbar.toolbar.setTitle(main.userQuery);
main.mainBinding.infoContainer.setVisibility(View.VISIBLE);
currentlyExecuting = new ProfileFetcher(main.userQuery, profileModel -> {
main.profileModel = profileModel;
if (profileModel == null) {
main.mainBinding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(main, R.string.error_loading_profile, Toast.LENGTH_SHORT).show();
main.mainBinding.toolbar.toolbar.setTitle(R.string.app_name);
return;
}
main.mainBinding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE);
final String profileId = profileModel.getId();
final boolean isLoggedIn = !Utils.isEmpty(Utils.settingsHelper.getString(Constants.COOKIE));
if (isLoggedIn) {
new StoryStatusFetcher(profileId, result -> {
main.storyModels = result;
if (result != null && result.length > 0) main.mainBinding.mainProfileImage.setStoriesBorder();
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
new HighlightsFetcher(profileId, result -> {
if (result != null && result.length > 0) {
main.mainBinding.highlightsList.setVisibility(View.VISIBLE);
main.highlightsAdapter.setData(result);
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
main.mainBinding.mainProfileImage.setEnabled(false);
Glide.with(main).load(profileModel.getSdProfilePic()).listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target<Drawable> target, final boolean isFirstResource) {
main.mainBinding.mainProfileImage.setEnabled(false);
return false;
}
@Override
public boolean onResourceReady(final Drawable resource, final Object model, final Target<Drawable> target, final DataSource dataSource, final boolean isFirstResource) {
main.mainBinding.mainProfileImage.setEnabled(true);
return false;
}
}).into(main.mainBinding.mainProfileImage);
final long followersCount = profileModel.getFollowersCount();
final long followingCount = profileModel.getFollowingCount();
final String postCount = String.valueOf(profileModel.getPostCount());
SpannableStringBuilder span = new SpannableStringBuilder(resources.getString(R.string.main_posts_count, postCount));
span.setSpan(new RelativeSizeSpan(1.2f), 0, postCount.length(), 0);
span.setSpan(new StyleSpan(Typeface.BOLD), 0, postCount.length(), 0);
main.mainBinding.mainPostCount.setText(span);
final String followersCountStr = String.valueOf(followersCount);
final int followersCountStrLen = followersCountStr.length();
span = new SpannableStringBuilder(resources.getString(R.string.main_posts_followers, followersCountStr));
span.setSpan(new RelativeSizeSpan(1.2f), 0, followersCountStrLen, 0);
span.setSpan(new StyleSpan(Typeface.BOLD), 0, followersCountStrLen, 0);
main.mainBinding.mainFollowers.setText(span);
final String followingCountStr = String.valueOf(followingCount);
final int followingCountStrLen = followingCountStr.length();
span = new SpannableStringBuilder(resources.getString(R.string.main_posts_following, followingCountStr));
span.setSpan(new RelativeSizeSpan(1.2f), 0, followingCountStrLen, 0);
span.setSpan(new StyleSpan(Typeface.BOLD), 0, followingCountStrLen, 0);
main.mainBinding.mainFollowing.setText(span);
main.mainBinding.mainFullName.setText(profileModel.getName());
CharSequence biography = profileModel.getBiography();
main.mainBinding.mainBiography.setCaptionIsExpandable(true);
main.mainBinding.mainBiography.setCaptionIsExpanded(true);
if (Utils.hasMentions(biography)) {
biography = Utils.getMentionText(biography);
main.mainBinding.mainBiography.setText(biography, TextView.BufferType.SPANNABLE);
main.mainBinding.mainBiography.setMentionClickListener(mentionClickListener);
} else {
main.mainBinding.mainBiography.setText(biography);
main.mainBinding.mainBiography.setMentionClickListener(null);
}
final String url = profileModel.getUrl();
if (Utils.isEmpty(url)) {
main.mainBinding.mainUrl.setVisibility(View.GONE);
} else {
main.mainBinding.mainUrl.setVisibility(View.VISIBLE);
main.mainBinding.mainUrl.setText(Utils.getSpannableUrl(url));
}
main.mainBinding.mainFullName.setSelected(true);
main.mainBinding.mainBiography.setEnabled(true);
if (!profileModel.isPrivate()) {
main.mainBinding.swipeRefreshLayout.setRefreshing(true);
main.mainBinding.mainPosts.setVisibility(View.VISIBLE);
main.mainBinding.privatePage.setVisibility(View.GONE);
if (isLoggedIn) {
final View.OnClickListener followClickListener = v -> main.startActivity(new Intent(main, FollowViewer.class)
.putExtra(Constants.EXTRAS_FOLLOWERS, v == main.mainBinding.mainFollowers)
.putExtra(Constants.EXTRAS_NAME, profileModel.getUsername())
.putExtra(Constants.EXTRAS_ID, profileId));
main.mainBinding.mainFollowers.setOnClickListener(followersCount > 0 ? followClickListener : null);
main.mainBinding.mainFollowing.setOnClickListener(followingCount > 0 ? followClickListener : null);
}
currentlyExecuting = new PostsFetcher(profileId, postsFetchListener).setUsername(profileModel.getUsername())
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
main.mainBinding.swipeRefreshLayout.setRefreshing(false);
main.mainBinding.privatePage.setVisibility(View.VISIBLE);
main.mainBinding.mainPosts.setVisibility(View.GONE);
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
public static void stopCurrentExecutor() {
if (currentlyExecuting != null) {
try {
currentlyExecuting.cancel(true);
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.MAIN_HELPER, "stopCurrentExecutor");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
}
}
private void toggleSelection(final PostModel postModel) {
if (postModel != null && postsAdapter != null) {
if (postModel.isSelected()) main.selectedItems.remove(postModel);
else main.selectedItems.add(postModel);
postModel.setSelected(!postModel.isSelected());
notifyAdapter(postModel);
}
}
private void notifyAdapter(final PostModel postModel) {
if (main.selectedItems.size() < 1) postsAdapter.isSelecting = false;
if (postModel.getPosition() < 0) postsAdapter.notifyDataSetChanged();
else postsAdapter.notifyItemChanged(postModel.getPosition(), postModel);
if (main.downloadAction != null) main.downloadAction.setVisible(postsAdapter.isSelecting);
}
///////////////////////////////////////////////////
private void toggleDiscoverSelection(final DiscoverItemModel itemModel) {
if (itemModel != null && discoverAdapter != null) {
if (itemModel.isSelected()) main.selectedDiscoverItems.remove(itemModel);
else main.selectedDiscoverItems.add(itemModel);
itemModel.setSelected(!itemModel.isSelected());
notifyDiscoverAdapter(itemModel);
}
}
private void notifyDiscoverAdapter(final DiscoverItemModel itemModel) {
if (main.selectedDiscoverItems.size() < 1) discoverAdapter.isSelecting = false;
if (itemModel.getPosition() < 0) discoverAdapter.notifyDataSetChanged();
else discoverAdapter.notifyItemChanged(itemModel.getPosition(), itemModel);
if (main.downloadAction != null) main.downloadAction.setVisible(discoverAdapter.isSelecting);
}
public boolean isSelectionCleared() {
if (postsAdapter != null && postsAdapter.isSelecting) {
for (final PostModel postModel : main.selectedItems) postModel.setSelected(false);
main.selectedItems.clear();
postsAdapter.isSelecting = false;
postsAdapter.notifyDataSetChanged();
if (main.downloadAction != null) main.downloadAction.setVisible(false);
return false;
} else if (discoverAdapter != null && discoverAdapter.isSelecting) {
for (final DiscoverItemModel itemModel : main.selectedDiscoverItems) itemModel.setSelected(false);
main.selectedDiscoverItems.clear();
discoverAdapter.isSelecting = false;
discoverAdapter.notifyDataSetChanged();
if (main.downloadAction != null) main.downloadAction.setVisible(false);
return false;
}
return true;
}
public void deselectSelection(final BasePostModel postModel) {
if (postModel instanceof PostModel) {
main.selectedItems.remove(postModel);
postModel.setSelected(false);
if (postsAdapter != null) notifyAdapter((PostModel) postModel);
} else if (postModel instanceof DiscoverItemModel) {
main.selectedDiscoverItems.remove(postModel);
postModel.setSelected(false);
if (discoverAdapter != null) notifyDiscoverAdapter((DiscoverItemModel) postModel);
}
}
public void onPause() {
if (currentFeedPlayer != null) {
currentFeedPlayer.setPlayWhenReady(false);
currentFeedPlayer.getPlaybackState();
}
}
public void onResume() {
if (currentFeedPlayer != null) {
currentFeedPlayer.setPlayWhenReady(true);
currentFeedPlayer.getPlaybackState();
}
}
}

11
app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.java

@ -0,0 +1,11 @@
package awais.instagrabber.activities;
import androidx.appcompat.app.AppCompatActivity;
import awais.instagrabber.utils.LocaleUtils;
public abstract class BaseLanguageActivity extends AppCompatActivity {
protected BaseLanguageActivity() {
LocaleUtils.updateConfig(this);
}
}

146
app/src/main/java/awais/instagrabber/activities/CommentsViewer.java

@ -0,0 +1,146 @@
package awais.instagrabber.activities;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.RelativeSizeSpan;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.CommentsAdapter;
import awais.instagrabber.asyncs.CommentsFetcher;
import awais.instagrabber.databinding.ActivityCommentsBinding;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.interfaces.MentionClickListener;
import awais.instagrabber.models.CommentModel;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
public final class CommentsViewer extends AppCompatActivity {
private CommentsAdapter commentsAdapter;
private CommentModel commentModel;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityCommentsBinding commentsBinding = ActivityCommentsBinding.inflate(getLayoutInflater());
setContentView(commentsBinding.getRoot());
final String shortCode;
final Intent intent = getIntent();
if (intent == null || !intent.hasExtra(Constants.EXTRAS_SHORTCODE)
|| Utils.isEmpty((shortCode = intent.getStringExtra(Constants.EXTRAS_SHORTCODE)))) {
Utils.errorFinish(this);
return;
}
setSupportActionBar(commentsBinding.toolbar.toolbar);
commentsBinding.toolbar.toolbar.setTitle(R.string.title_comments);
commentsBinding.toolbar.toolbar.setSubtitle(shortCode);
final Resources resources = getResources();
final ArrayAdapter<String> commmentDialogAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1,
new String[]{resources.getString(R.string.open_profile),
resources.getString(R.string.view_pfp),
resources.getString(R.string.comment_viewer_copy_user),
resources.getString(R.string.comment_viewer_copy_comment)});
final DialogInterface.OnClickListener profileDialogListener = (dialog, which) -> {
final ProfileModel profileModel = commentModel.getProfileModel();
if (which == 0) {
searchUsername(profileModel.getUsername());
} else if (which == 1) {
startActivity(new Intent(this, ProfileViewer.class).putExtra(Constants.EXTRAS_PROFILE, profileModel));
} else if (which == 2) {
Utils.copyText(this, profileModel.getUsername());
} else if (which == 3) {
Utils.copyText(this, commentModel.getText().toString());
}
};
final View.OnClickListener clickListener = v -> {
final Object tag = v.getTag();
if (tag instanceof CommentModel) {
commentModel = (CommentModel) tag;
final String username = commentModel.getProfileModel().getUsername();
final SpannableString title = new SpannableString(username + ":\n" + commentModel.getText());
title.setSpan(new RelativeSizeSpan(1.23f), 0, username.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
new AlertDialog.Builder(this).setTitle(title)
.setAdapter(commmentDialogAdapter, profileDialogListener)
.setNeutralButton(R.string.cancel, null)
.show();
}
};
final MentionClickListener mentionClickListener = (view, text, isHashtag) ->
new AlertDialog.Builder(this).setTitle(text)
.setMessage(isHashtag ? R.string.comment_view_mention_hash_search : R.string.comment_view_mention_user_search)
.setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok,
(dialog, which) -> searchUsername(text)).show();
new CommentsFetcher(shortCode, new FetchListener<CommentModel[]>() {
@Override
public void doBefore() {
commentsBinding.toolbar.progressCircular.setVisibility(View.VISIBLE);
}
@Override
public void onResult(final CommentModel[] commentModels) {
commentsBinding.toolbar.progressCircular.setVisibility(View.GONE);
commentsAdapter = new CommentsAdapter(commentModels, true, clickListener, mentionClickListener);
commentsBinding.rvComments.setAdapter(commentsAdapter);
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void searchUsername(final String text) {
if (Main.scanHack != null) {
Main.scanHack.onResult(text);
setResult(6969);
finish();
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.follow, menu);
final MenuItem menuSearch = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) menuSearch.getActionView();
searchView.setQueryHint(getResources().getString(R.string.action_search));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(final String query) {
return false;
}
@Override
public boolean onQueryTextChange(final String query) {
if (commentsAdapter != null) commentsAdapter.getFilter().filter(query);
return true;
}
});
menu.findItem(R.id.action_compare).setVisible(false);
return true;
}
}

113
app/src/main/java/awais/instagrabber/activities/DirectMessages.java

@ -0,0 +1,113 @@
package awais.instagrabber.activities;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.util.ArrayList;
import java.util.Arrays;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.adapters.DirectMessagesAdapter;
import awais.instagrabber.asyncs.direct_messages.InboxFetcher;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader;
import awais.instagrabber.databinding.ActivityDmsBinding;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.direct_messages.InboxModel;
import awais.instagrabber.models.direct_messages.InboxThreadModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
public final class DirectMessages extends BaseLanguageActivity implements SwipeRefreshLayout.OnRefreshListener {
private final ArrayList<InboxThreadModel> inboxThreadModelList = new ArrayList<>();
private final DirectMessagesAdapter messagesAdapter = new DirectMessagesAdapter(inboxThreadModelList, v -> {
final Object tag = v.getTag();
if (tag instanceof InboxThreadModel) {
startActivity(new Intent(this, DirectMessagesUserInbox.class)
.putExtra(Constants.EXTRAS_THREAD_MODEL, (InboxThreadModel) tag)
);
}
});
private final FetchListener<InboxModel> fetchListener = new FetchListener<InboxModel>() {
@Override
public void doBefore() {
dmsBinding.swipeRefreshLayout.setRefreshing(true);
}
@Override
public void onResult(final InboxModel inboxModel) {
if (inboxModel != null) {
endCursor = inboxModel.getOldestCursor();
if ("MINCURSOR".equals(endCursor) || "MAXCURSOR".equals(endCursor)) endCursor = null;
// todo get request / unseen count from inboxModel
final InboxThreadModel[] threads = inboxModel.getThreads();
if (threads != null) {
final int oldSize = inboxThreadModelList.size();
inboxThreadModelList.addAll(Arrays.asList(threads));
messagesAdapter.notifyItemRangeInserted(oldSize, threads.length);
}
}
dmsBinding.swipeRefreshLayout.setRefreshing(false);
stopCurrentExecutor();
}
};
private String endCursor;
private RecyclerLazyLoader lazyLoader;
private AsyncTask<Void, Void, InboxModel> currentlyRunning;
private ActivityDmsBinding dmsBinding;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dmsBinding = ActivityDmsBinding.inflate(getLayoutInflater());
setContentView(dmsBinding.getRoot());
dmsBinding.swipeRefreshLayout.setOnRefreshListener(this);
final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
dmsBinding.rvDirectMessages.setLayoutManager(layoutManager);
dmsBinding.rvDirectMessages.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
dmsBinding.rvDirectMessages.setAdapter(messagesAdapter);
lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
if (!Utils.isEmpty(endCursor))
currentlyRunning = new InboxFetcher(endCursor, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
endCursor = null;
});
dmsBinding.rvDirectMessages.addOnScrollListener(lazyLoader);
stopCurrentExecutor();
currentlyRunning = new InboxFetcher(null, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public void onRefresh() {
endCursor = null;
lazyLoader.resetState();
inboxThreadModelList.clear();
messagesAdapter.notifyDataSetChanged();
stopCurrentExecutor();
currentlyRunning = new InboxFetcher(null, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void stopCurrentExecutor() {
if (currentlyRunning != null) {
try {
currentlyRunning.cancel(true);
} catch (final Exception e) {
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
}
}
}

101
app/src/main/java/awais/instagrabber/activities/DirectMessagesUserInbox.java

@ -0,0 +1,101 @@
package awais.instagrabber.activities;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.MessageItemsAdapter;
import awais.instagrabber.asyncs.direct_messages.UserInboxFetcher;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader;
import awais.instagrabber.databinding.ActivityDmsBinding;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.models.direct_messages.DirectItemModel;
import awais.instagrabber.models.direct_messages.InboxThreadModel;
import awais.instagrabber.models.enums.UserInboxDirection;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
public final class DirectMessagesUserInbox extends AppCompatActivity {
private final ArrayList<ProfileModel> users = new ArrayList<>();
private final ArrayList<DirectItemModel> directItemModels = new ArrayList<>();
private final FetchListener<InboxThreadModel> fetchListener = new FetchListener<InboxThreadModel>() {
@Override
public void doBefore() {
dmsBinding.swipeRefreshLayout.setRefreshing(true);
}
@Override
public void onResult(final InboxThreadModel result) {
if (result == null && "MINCURSOR".equals(endCursor) || "MAXCURSOR".equals(endCursor) || Utils.isEmpty(endCursor))
Toast.makeText(DirectMessagesUserInbox.this, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
if (result != null) {
endCursor = result.getPrevCursor();
if ("MINCURSOR".equals(endCursor) || "MAXCURSOR".equals(endCursor)) endCursor = null;
users.clear();
users.addAll(Arrays.asList(result.getUsers()));
final int oldSize = directItemModels.size();
final List<DirectItemModel> itemModels = Arrays.asList(result.getItems());
directItemModels.addAll(itemModels);
messageItemsAdapter.notifyItemRangeInserted(oldSize, itemModels.size());
}
dmsBinding.swipeRefreshLayout.setRefreshing(false);
}
};
private String endCursor;
private ActivityDmsBinding dmsBinding;
private MessageItemsAdapter messageItemsAdapter;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dmsBinding = ActivityDmsBinding.inflate(getLayoutInflater());
setContentView(dmsBinding.getRoot());
final InboxThreadModel threadModel;
final Intent intent = getIntent();
if (intent == null || !intent.hasExtra(Constants.EXTRAS_THREAD_MODEL) ||
(threadModel = (InboxThreadModel) intent.getSerializableExtra(Constants.EXTRAS_THREAD_MODEL)) == null) {
Utils.errorFinish(this);
return;
}
dmsBinding.swipeRefreshLayout.setEnabled(false);
final LinearLayoutManager layoutManager = new LinearLayoutManager(this, RecyclerView.VERTICAL, true);
dmsBinding.rvDirectMessages.setLayoutManager(layoutManager);
dmsBinding.rvDirectMessages.addOnScrollListener(new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
if (!Utils.isEmpty(endCursor)) {
new UserInboxFetcher(threadModel.getThreadId(), UserInboxDirection.OLDER,
endCursor, fetchListener).execute(); // serial because we don't want messages to be randomly ordered
}
}));
dmsBinding.rvDirectMessages.setAdapter(messageItemsAdapter = new MessageItemsAdapter(directItemModels, users, v -> {
// todo do something with clicked message
Log.d("AWAISKING_APP", "--> " + v.getTag());
}, (view, text, isHashtag) -> {
// todo mention click stuff
}));
new UserInboxFetcher(threadModel.getThreadId(), UserInboxDirection.OLDER, null, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}

348
app/src/main/java/awais/instagrabber/activities/FollowViewer.java

@ -0,0 +1,348 @@
package awais.instagrabber.activities;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.util.ArrayList;
import java.util.Arrays;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FollowAdapter;
import awais.instagrabber.asyncs.FollowFetcher;
import awais.instagrabber.databinding.ActivityFollowBinding;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import thoughtbot.expandableadapter.ExpandableGroup;
import static awais.instagrabber.utils.Utils.logCollector;
public final class FollowViewer extends BaseLanguageActivity implements SwipeRefreshLayout.OnRefreshListener {
private final ArrayList<FollowModel> followModels = new ArrayList<>();
private final ArrayList<FollowModel> followingModels = new ArrayList<>();
private final ArrayList<FollowModel> followersModels = new ArrayList<>();
private final ArrayList<FollowModel> allFollowing = new ArrayList<>();
private boolean followers, isCompare = false;
private String id, name, namePost, type;
private Resources resources;
private FollowModel model;
private FollowAdapter adapter;
private View.OnClickListener clickListener;
private ActivityFollowBinding followBinding;
private AsyncTask<Void, Void, FollowModel[]> currentlyExecuting;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
followBinding = ActivityFollowBinding.inflate(getLayoutInflater());
setContentView(followBinding.getRoot());
final Intent intent = getIntent();
if (intent == null || Utils.isEmpty(id = intent.getStringExtra(Constants.EXTRAS_ID))) {
Utils.errorFinish(this);
return;
}
setSupportActionBar(followBinding.toolbar.toolbar);
followers = intent.getBooleanExtra(Constants.EXTRAS_FOLLOWERS, false);
name = intent.getStringExtra(Constants.EXTRAS_NAME);
namePost = name + " is";
if (Utils.isEmpty(name)) {
name = "You";
namePost = "You're";
}
followBinding.toolbar.toolbar.setTitle(name);
resources = getResources();
final ArrayAdapter<Object> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new String[]{
resources.getString(R.string.open_profile), resources.getString(R.string.followers_open_in_insta)});
final AlertDialog alertDialog = new AlertDialog.Builder(this).setAdapter(adapter, (dialog, which) -> {
if (model != null) {
if (which == 0) {
if (Main.scanHack != null) {
Main.scanHack.onResult(model.getUsername());
finish();
}
} else {
final Intent actIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://instagram.com/" + model.getUsername()));
if (Utils.isInstagramInstalled) actIntent.setPackage("com.instagram.android");
startActivity(actIntent);
}
}
}).setTitle(R.string.what_to_do_dialog).create();
clickListener = v -> {
final Object tag = v.getTag();
if (tag instanceof FollowModel) {
model = (FollowModel) tag;
if (!alertDialog.isShowing()) alertDialog.show();
}
};
followBinding.swipeRefreshLayout.setOnRefreshListener(this);
onRefresh();
}
@Override
public void onRefresh() {
if (isCompare) listCompare();
else listFollows();
}
private void listFollows() {
stopCurrentExecutor();
type = resources.getString(followers ? R.string.followers_type_followers : R.string.followers_type_following);
followBinding.toolbar.toolbar.setSubtitle(type);
followModels.clear();
final FetchListener<FollowModel[]> fetchListener = new FetchListener<FollowModel[]>() {
@Override
public void doBefore() {
followBinding.swipeRefreshLayout.setRefreshing(true);
}
@Override
public void onResult(final FollowModel[] result) {
if (result == null) followBinding.swipeRefreshLayout.setRefreshing(false);
else {
followModels.addAll(Arrays.asList(result));
final FollowModel model = result[result.length - 1];
if (model != null && model.hasNextPage()) {
stopCurrentExecutor();
currentlyExecuting = new FollowFetcher(id, followers, model.getEndCursor(), this)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
model.setPageCursor(false, null);
} else {
followBinding.swipeRefreshLayout.setRefreshing(false);
refreshAdapter(followModels, null, null, null);
}
}
}
};
currentlyExecuting = new FollowFetcher(id, followers, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void listCompare() {
stopCurrentExecutor();
followBinding.toolbar.toolbar.setSubtitle(R.string.followers_compare);
allFollowing.clear();
followersModels.clear();
followingModels.clear();
final FetchListener<FollowModel[]> followingFetchListener = new FetchListener<FollowModel[]>() {
@Override
public void onResult(final FollowModel[] result) {
if (result != null) {
followingModels.addAll(Arrays.asList(result));
final FollowModel model = result[result.length - 1];
if (model != null && model.hasNextPage()) {
stopCurrentExecutor();
currentlyExecuting = new FollowFetcher(id, false, model.getEndCursor(), this)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
model.setPageCursor(false, null);
} else {
allFollowing.addAll(followersModels);
allFollowing.retainAll(followingModels);
for (final FollowModel followModel : allFollowing) {
followersModels.remove(followModel);
followingModels.remove(followModel);
}
allFollowing.trimToSize();
followersModels.trimToSize();
followingModels.trimToSize();
followBinding.swipeRefreshLayout.setRefreshing(false);
refreshAdapter(null, followingModels, followersModels, allFollowing);
}
} else followBinding.swipeRefreshLayout.setRefreshing(false);
}
};
final FetchListener<FollowModel[]> followersFetchListener = new FetchListener<FollowModel[]>() {
@Override
public void doBefore() {
followBinding.swipeRefreshLayout.setRefreshing(true);
}
@Override
public void onResult(final FollowModel[] result) {
if (result != null) {
followersModels.addAll(Arrays.asList(result));
final FollowModel model = result[result.length - 1];
if (model == null || !model.hasNextPage()) {
stopCurrentExecutor();
currentlyExecuting = new FollowFetcher(id, false, followingFetchListener)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
stopCurrentExecutor();
currentlyExecuting = new FollowFetcher(id, true, model.getEndCursor(), this)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
model.setPageCursor(false, null);
}
}
}
};
currentlyExecuting = new FollowFetcher(id, true, followersFetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.follow, menu);
final MenuItem menuSearch = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) menuSearch.getActionView();
searchView.setQueryHint(getResources().getString(R.string.action_search));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
// private final Filter filter = new Filter() {
// private final ArrayList<FollowModel> searchFollowModels = new ArrayList<>(followModels.size() / 2);
// private final ArrayList<FollowModel> searchFollowingModels = new ArrayList<>(followingModels.size() / 2);
// private final ArrayList<FollowModel> searchFollowersModels = new ArrayList<>(followersModels.size() / 2);
// private final ArrayList<FollowModel> searchAllFollowing = new ArrayList<>(allFollowing.size() / 2);
//
// @Nullable
// @Override
// protected FilterResults performFiltering(@NonNull final CharSequence constraint) {
// searchFollowModels.clear();
// searchFollowingModels.clear();
// searchFollowersModels.clear();
// searchAllFollowing.clear();
//
// final int followModelsSize = followModels.size();
// final int followingModelsSize = followingModels.size();
// final int followersModelsSize = followersModels.size();
// final int allFollowingSize = allFollowing.size();
//
// int maxSize = followModelsSize;
// if (maxSize < followingModelsSize) maxSize = followingModelsSize;
// if (maxSize < followersModelsSize) maxSize = followersModelsSize;
// if (maxSize < allFollowingSize) maxSize = allFollowingSize;
//
// final String query = constraint.toString().toLowerCase();
// FollowModel followModel;
// while (maxSize != -1) {
// if (maxSize < followModelsSize) {
// followModel = followModels.get(maxSize);
// if (Utils.hasKey(query, followModel.getUsername(), followModel.getFullName()))
// searchFollowModels.add(followModel);
// }
//
// if (maxSize < followingModelsSize) {
// followModel = followingModels.get(maxSize);
// if (Utils.hasKey(query, followModel.getUsername(), followModel.getFullName()))
// searchFollowingModels.add(followModel);
// }
//
// if (maxSize < followersModelsSize) {
// followModel = followersModels.get(maxSize);
// if (Utils.hasKey(query, followModel.getUsername(), followModel.getFullName()))
// searchFollowersModels.add(followModel);
// }
//
// if (maxSize < allFollowingSize) {
// followModel = allFollowing.get(maxSize);
// if (Utils.hasKey(query, followModel.getUsername(), followModel.getFullName()))
// searchAllFollowing.add(followModel);
// }
//
// --maxSize;
// }
//
// return null;
// }
//
// @Override
// protected void publishResults(final CharSequence query, final FilterResults results) {
// refreshAdapter(searchFollowModels, searchFollowingModels, searchFollowersModels, searchAllFollowing);
// }
// };
@Override
public boolean onQueryTextSubmit(final String query) {
return false;
}
@Override
public boolean onQueryTextChange(final String query) {
// if (Utils.isEmpty(query)) refreshAdapter(followModels, followingModels, followersModels, allFollowing);
// else filter.filter(query.toLowerCase());
if (adapter != null) adapter.getFilter().filter(query);
return true;
}
});
final MenuItem menuCompare = menu.findItem(R.id.action_compare);
menuCompare.setOnMenuItemClickListener(item -> {
followBinding.rvFollow.setAdapter(null);
if (isCompare) listFollows();
else listCompare();
isCompare = !isCompare;
return true;
});
return true;
}
private void refreshAdapter(final ArrayList<FollowModel> followModels, final ArrayList<FollowModel> followingModels,
final ArrayList<FollowModel> followersModels, final ArrayList<FollowModel> allFollowing) {
final ArrayList<ExpandableGroup> groups = new ArrayList<>(1);
if (isCompare) {
if (followingModels.size() > 0)
groups.add(new ExpandableGroup(resources.getString(R.string.followers_not_following, name), followingModels));
if (followersModels.size() > 0)
groups.add(new ExpandableGroup(resources.getString(R.string.followers_not_follower, namePost), followersModels));
if (allFollowing.size() > 0)
groups.add(new ExpandableGroup(resources.getString(R.string.followers_both_following), allFollowing));
} else {
final ExpandableGroup group = new ExpandableGroup(type, followModels);
groups.add(group);
}
adapter = new FollowAdapter(this, clickListener, groups);
adapter.toggleGroup(0);
followBinding.rvFollow.setAdapter(adapter);
}
public void stopCurrentExecutor() {
if (currentlyExecuting != null) {
try {
currentlyExecuting.cancel(true);
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.MAIN_HELPER, "stopCurrentExecutor");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
}
}
}

130
app/src/main/java/awais/instagrabber/activities/Login.java

@ -0,0 +1,130 @@
package awais.instagrabber.activities;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.CompoundButton;
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.Utils;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class Login extends BaseLanguageActivity implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
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;
}
};
private final WebChromeClient webChromeClient = new WebChromeClient();
private String webViewUrl, defaultUserAgent;
private ActivityLoginBinding loginBinding;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loginBinding = ActivityLoginBinding.inflate(getLayoutInflater());
setContentView(loginBinding.getRoot());
initWebView();
loginBinding.desktopMode.setOnCheckedChangeListener(this);
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/");
} else if (v == loginBinding.cookies) {
final String mainCookie = Utils.getCookie(webViewUrl);
if (Utils.isEmpty(mainCookie))
Toast.makeText(this, R.string.login_error_loading_cookies, Toast.LENGTH_SHORT).show();
else {
Utils.setupCookies(mainCookie);
settingsHelper.putString(Constants.COOKIE, mainCookie);
Toast.makeText(this, R.string.login_success_loading_cookies, Toast.LENGTH_SHORT).show();
finish();
}
}
}
@Override
public void onCheckedChanged(final CompoundButton buttonView, final boolean isChecked) {
final WebSettings webSettings = loginBinding.webView.getSettings();
final String newUserAgent = isChecked ? "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36"
: defaultUserAgent;
webSettings.setUserAgentString(newUserAgent);
webSettings.setUseWideViewPort(isChecked);
webSettings.setLoadWithOverviewMode(isChecked);
webSettings.setSupportZoom(isChecked);
webSettings.setBuiltInZoomControls(isChecked);
loginBinding.webView.loadUrl("https://instagram.com/");
}
@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) {
if (defaultUserAgent == null) defaultUserAgent = webSettings.getUserAgentString();
webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDisplayZoomControls(false);
webSettings.setLoadWithOverviewMode(true);
webSettings.setUseWideViewPort(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
webSettings.setAllowFileAccessFromFileURLs(true);
webSettings.setAllowUniversalAccessFromFileURLs(true);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
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();
}
}

480
app/src/main/java/awais/instagrabber/activities/Main.java

@ -0,0 +1,480 @@
package awais.instagrabber.activities;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.MatrixCursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.provider.BaseColumns;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.GridLayoutManager;
import java.util.ArrayList;
import java.util.Stack;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.MainHelper;
import awais.instagrabber.R;
import awais.instagrabber.adapters.HighlightsAdapter;
import awais.instagrabber.adapters.SuggestionsAdapter;
import awais.instagrabber.asyncs.SuggestionsFetcher;
import awais.instagrabber.asyncs.UsernameFetcher;
import awais.instagrabber.customviews.MouseDrawer;
import awais.instagrabber.databinding.ActivityMainBinding;
import awais.instagrabber.dialogs.AboutDialog;
import awais.instagrabber.dialogs.QuickAccessDialog;
import awais.instagrabber.dialogs.SettingsDialog;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.interfaces.ItemGetter;
import awais.instagrabber.models.DiscoverItemModel;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.HighlightModel;
import awais.instagrabber.models.PostModel;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.SuggestionModel;
import awais.instagrabber.models.enums.DownloadMethod;
import awais.instagrabber.models.enums.ItemGetType;
import awais.instagrabber.models.enums.SuggestionType;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DataBox;
import awais.instagrabber.utils.FlavorTown;
import awais.instagrabber.utils.MyApps;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class Main extends BaseLanguageActivity {
public static FetchListener<String> scanHack;
public static ItemGetter itemGetter;
// -------- items --------
public final ArrayList<PostModel> allItems = new ArrayList<>();
public final ArrayList<FeedModel> feedItems = new ArrayList<>();
public final ArrayList<DiscoverItemModel> discoverItems = new ArrayList<>();
// -------- items --------
public final ArrayList<PostModel> selectedItems = new ArrayList<>();
public final ArrayList<DiscoverItemModel> selectedDiscoverItems = new ArrayList<>();
// -------- items --------
public final HighlightsAdapter highlightsAdapter = new HighlightsAdapter(null, new View.OnClickListener() {
@Override
public void onClick(final View v) {
final Object tag = v.getTag();
if (tag instanceof HighlightModel) {
final HighlightModel highlightModel = (HighlightModel) tag;
startActivity(new Intent(Main.this, StoryViewer.class)
.putExtra(Constants.EXTRAS_USERNAME, userQuery)
.putExtra(Constants.EXTRAS_HIGHLIGHT, highlightModel.getTitle())
.putExtra(Constants.EXTRAS_STORIES, highlightModel.getStoryModels()));
}
}
});
private SuggestionsAdapter suggestionAdapter;
private MenuItem searchAction;
public ActivityMainBinding mainBinding;
public SearchView searchView;
public MenuItem downloadAction, settingsAction, dmsAction;
public StoryModel[] storyModels;
public String userQuery = null;
public MainHelper mainHelper;
public ProfileModel profileModel;
private AutoCompleteTextView searchAutoComplete;
private ArrayAdapter<String> profileDialogAdapter;
private DialogInterface.OnClickListener profileDialogListener;
private Stack<String> queriesStack;
public Main() {
super();
Utils.changeTheme();
}
@Override
protected void onCreate(@Nullable final Bundle bundle) {
super.onCreate(bundle);
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mainBinding.getRoot());
FlavorTown.updateCheck(this);
FlavorTown.changelogCheck(this);
final String cookie = settingsHelper.getString(Constants.COOKIE);
final String uid = Utils.getUserIdFromCookie(cookie);
Utils.setupCookies(cookie);
MainHelper.stopCurrentExecutor();
mainHelper = new MainHelper(this);
if (bundle == null) {
queriesStack = new Stack<>();
userQuery = null;
} else {
setStack(bundle);
userQuery = bundle.getString("query");
}
itemGetter = itemGetType -> {
if (itemGetType == ItemGetType.MAIN_ITEMS) return allItems;
if (itemGetType == ItemGetType.DISCOVER_ITEMS) return discoverItems;
if (itemGetType == ItemGetType.FEED_ITEMS) return feedItems;
return null;
};
scanHack = result -> {
if (mainHelper != null && !Utils.isEmpty(result)) {
closeAnyOpenDrawer();
addToStack();
userQuery = result;
mainHelper.onRefresh();
}
};
// searches for your userid and returns username
if (uid != null) {
final FetchListener<String> fetchListener = username -> {
if (!Utils.isEmpty(username)) {
if (!BuildConfig.DEBUG) {
userQuery = username;
if (mainHelper != null && !mainBinding.swipeRefreshLayout.isRefreshing()) mainHelper.onRefresh();
}
// adds cookies to database for quick access
final DataBox.CookieModel cookieModel = Utils.dataBox.getCookie(uid);
if (Utils.dataBox.getCookieCount() == 0 || cookieModel == null || Utils.isEmpty(cookieModel.getUsername()))
Utils.dataBox.addUserCookie(new DataBox.CookieModel(uid, username, cookie));
}
};
boolean found = false;
final DataBox.CookieModel cookieModel = Utils.dataBox.getCookie(uid);
if (cookieModel != null) {
final String username = cookieModel.getUsername();
if (username != null) {
found = true;
fetchListener.onResult(username);
}
}
if (!found) // if not in database, fetch info from instagram
new UsernameFetcher(uid, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
suggestionAdapter = new SuggestionsAdapter(this, v -> {
final Object tag = v.getTag();
if (tag instanceof CharSequence) {
addToStack();
userQuery = tag.toString();
mainHelper.onRefresh();
}
if (searchView != null && !searchView.isIconified()) {
if (searchAction != null) searchAction.collapseActionView();
searchView.setIconified(true);
searchView.setIconified(true);
}
});
final Resources resources = getResources();
profileDialogAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1,
new String[]{resources.getString(R.string.view_pfp), resources.getString(R.string.show_stories)});
profileDialogListener = (dialog, which) -> {
final Intent intent;
if (which == 0 || storyModels == null || storyModels.length < 1)
intent = new Intent(this, ProfileViewer.class).putExtra(Constants.EXTRAS_PROFILE, profileModel);
else intent = new Intent(this, StoryViewer.class).putExtra(Constants.EXTRAS_USERNAME, userQuery)
.putExtra(Constants.EXTRAS_STORIES, storyModels);
startActivity(intent);
};
final View.OnClickListener onClickListener = v -> {
if (v == mainBinding.mainBiography) {
Utils.copyText(this, mainBinding.mainBiography.getText().toString());
} else if (v == mainBinding.mainProfileImage) {
if (storyModels == null || storyModels.length <= 0) {
profileDialogListener.onClick(null, 0);
} else {
// because sometimes configuration changes made this crash on some phones
new AlertDialog.Builder(this).setAdapter(profileDialogAdapter, profileDialogListener)
.setNeutralButton(R.string.cancel, null).show();
}
}
};
mainBinding.mainBiography.setOnClickListener(onClickListener);
mainBinding.mainProfileImage.setOnClickListener(onClickListener);
mainBinding.mainBiography.setEnabled(false);
mainBinding.mainProfileImage.setEnabled(false);
final boolean isQueryNull = userQuery == null;
if (isQueryNull) allItems.clear();
if (BuildConfig.DEBUG && isQueryNull) userQuery = "the.badak"; // todo
if (!mainBinding.swipeRefreshLayout.isRefreshing() && userQuery != null) mainHelper.onRefresh();
mainHelper.onIntent(getIntent());
}
private void downloadSelectedItems() {
if (selectedItems.size() > 0) {
Utils.batchDownload(this, userQuery, DownloadMethod.DOWNLOAD_MAIN, selectedItems);
} else if (selectedDiscoverItems.size() > 0) {
Utils.batchDownload(this, null, DownloadMethod.DOWNLOAD_DISCOVER, selectedDiscoverItems);
}
}
@Override
protected void onNewIntent(final Intent intent) {
super.onNewIntent(intent);
mainHelper.onIntent(intent);
}
@Override
public void onSaveInstanceState(@NonNull final Bundle outState, @NonNull final PersistableBundle outPersistentState) {
outState.putString("query", userQuery);
outState.putSerializable("stack", queriesStack);
super.onSaveInstanceState(outState, outPersistentState);
}
@Override
public void onRestoreInstanceState(@Nullable final Bundle savedInstanceState, @Nullable final PersistableBundle persistentState) {
super.onRestoreInstanceState(savedInstanceState, persistentState);
if (savedInstanceState != null) {
userQuery = savedInstanceState.getString("query");
setStack(savedInstanceState);
}
}
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
outState.putString("query", userQuery);
outState.putSerializable("stack", queriesStack);
super.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
userQuery = savedInstanceState.getString("query");
setStack(savedInstanceState);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
final FragmentManager fragmentManager = getSupportFragmentManager();
final MenuItem quickAccessAction = menu.findItem(R.id.action_quickaccess).setVisible(true);
final MenuItem.OnMenuItemClickListener clickListener = item -> {
if (item == downloadAction) {
downloadSelectedItems();
} else if (item == dmsAction)
startActivity(new Intent(this, DirectMessages.class));
else if (item == settingsAction)
new SettingsDialog().show(fragmentManager, "settings");
else if (item == quickAccessAction)
new QuickAccessDialog().setQuery(userQuery).show(fragmentManager, "quickAccess");
else
new AboutDialog().show(fragmentManager, "about");
return true;
};
quickAccessAction.setOnMenuItemClickListener(clickListener);
menu.findItem(R.id.action_about).setVisible(true).setOnMenuItemClickListener(clickListener);
dmsAction = menu.findItem(R.id.action_dms).setOnMenuItemClickListener(clickListener);
settingsAction = menu.findItem(R.id.action_settings).setVisible(true).setOnMenuItemClickListener(clickListener);
downloadAction = menu.findItem(R.id.action_download).setOnMenuItemClickListener(clickListener);
if (!Utils.isEmpty(Utils.settingsHelper.getString(Constants.COOKIE))) {
settingsAction.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
dmsAction.setVisible(true).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
searchAction = menu.findItem(R.id.action_search);
searchView = (SearchView) searchAction.getActionView();
final View searchText = searchView.findViewById(R.id.search_src_text);
if (searchText instanceof AutoCompleteTextView)
searchAutoComplete = (AutoCompleteTextView) searchText;
searchView.setQueryHint(getResources().getString(R.string.action_search));
searchView.setSuggestionsAdapter(suggestionAdapter);
searchView.setOnSearchClickListener(v -> searchView.setQuery(userQuery, false));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
private boolean searchUser, searchHash;
private AsyncTask<?, ?, ?> prevSuggestionAsync;
private final String[] COLUMNS = {BaseColumns._ID, Constants.EXTRAS_USERNAME, Constants.EXTRAS_NAME,
Constants.EXTRAS_TYPE, "pfp", "verified"};
private final FetchListener<SuggestionModel[]> fetchListener = new FetchListener<SuggestionModel[]>() {
@Override
public void doBefore() {
suggestionAdapter.changeCursor(null);
}
@Override
public void onResult(final SuggestionModel[] result) {
final MatrixCursor cursor;
if (result == null) cursor = null;
else {
cursor = new MatrixCursor(COLUMNS, 0);
for (int i = 0; i < result.length; i++) {
final SuggestionModel suggestionModel = result[i];
if (suggestionModel != null) {
final SuggestionType suggestionType = suggestionModel.getSuggestionType();
final Object[] objects = {i, suggestionModel.getUsername(), suggestionModel.getName(),
suggestionType, suggestionModel.getProfilePic(), suggestionModel.isVerified()};
if (!searchHash && !searchUser) cursor.addRow(objects);
else {
final boolean isCurrHash = suggestionType == SuggestionType.TYPE_HASHTAG;
if (searchHash && isCurrHash || !searchHash && !isCurrHash)
cursor.addRow(objects);
}
}
}
}
suggestionAdapter.changeCursor(cursor);
}
};
private void cancelSuggestionsAsync() {
if (prevSuggestionAsync != null)
try { prevSuggestionAsync.cancel(true); } catch (final Exception ignored) { }
}
@Override
public boolean onQueryTextSubmit(final String query) {
cancelSuggestionsAsync();
closeAnyOpenDrawer();
addToStack();
userQuery = query;
searchAction.collapseActionView();
searchView.setIconified(true);
searchView.setIconified(true);
mainHelper.onRefresh();
return false;
}
@Override
public boolean onQueryTextChange(final String newText) {
cancelSuggestionsAsync();
if (!Utils.isEmpty(newText)) {
searchUser = newText.charAt(0) == '@';
searchHash = newText.charAt(0) == '#';
if (newText.length() == 1 && (searchHash || searchUser)) {
if (searchAutoComplete != null) searchAutoComplete.setThreshold(2);
} else {
if (searchAutoComplete != null) searchAutoComplete.setThreshold(1);
prevSuggestionAsync = new SuggestionsFetcher(fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
searchUser || searchHash ? newText.substring(1) : newText);
}
}
return true;
}
});
return true;
}
@Override
public void onBackPressed() {
if (closeAnyOpenDrawer()) return;
if (searchView != null && !searchView.isIconified()) {
if (searchAction != null) searchAction.collapseActionView();
searchView.setIconified(true);
searchView.setIconified(true);
return;
}
if (!mainHelper.isSelectionCleared()) return;
final GridLayoutManager layoutManager = (GridLayoutManager) mainBinding.mainPosts.getLayoutManager();
if (layoutManager != null && layoutManager.findFirstCompletelyVisibleItemPosition() >= layoutManager.getSpanCount()) {
mainBinding.mainPosts.smoothScrollToPosition(0);
mainBinding.appBarLayout.setExpanded(true, true);
return;
}
if (queriesStack != null && queriesStack.size() > 0) {
userQuery = queriesStack.pop();
if (userQuery != null) {
mainHelper.onRefresh();
return;
}
}
MyApps.showAlertDialog(this, (parent, view, position, id) -> {
if (id == -1 && position == -1 && parent == null) super.onBackPressed();
else MyApps.openAppStore(this, position);
});
}
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 8020 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
downloadSelectedItems();
}
@Override
protected void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 9629 && (resultCode == 1692 || resultCode == RESULT_CANCELED))
finish();
else if (requestCode == 6007)
Utils.showImportExportDialog(this);
else if (requestCode == 6969 && mainHelper.currentFeedPlayer != null)
mainHelper.currentFeedPlayer.setPlayWhenReady(true);
}
@Override
protected void onPause() {
if (mainHelper != null) mainHelper.onPause();
super.onPause();
}
@Override
protected void onResume() {
if (mainHelper != null) mainHelper.onResume();
super.onResume();
}
private void setStack(final Bundle bundle) {
final Object stack = bundle != null ? bundle.get("stack") : null;
if (stack instanceof Stack) //noinspection unchecked
queriesStack = (Stack<String>) stack;
}
public void addToStack() {
if (userQuery != null) {
if (queriesStack == null) queriesStack = new Stack<>();
queriesStack.add(userQuery);
}
}
private boolean closeAnyOpenDrawer() {
final int childCount = mainBinding.drawerLayout.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = mainBinding.drawerLayout.getChildAt(i);
final MouseDrawer.LayoutParams childLp = (MouseDrawer.LayoutParams) child.getLayoutParams();
if ((childLp.openState & MouseDrawer.LayoutParams.FLAG_IS_OPENED) == 1 ||
(childLp.openState & MouseDrawer.LayoutParams.FLAG_IS_OPENING) == 2 ||
childLp.onScreen >= 0.6 || childLp.isPeeking) {
mainBinding.drawerLayout.closeDrawer(child);
return true;
}
}
return false;
}
}

639
app/src/main/java/awais/instagrabber/activities/PostViewer.java

@ -0,0 +1,639 @@
package awais.instagrabber.activities;
import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.PostsMediaAdapter;
import awais.instagrabber.asyncs.PostFetcher;
import awais.instagrabber.asyncs.ProfileFetcher;
import awais.instagrabber.customviews.CommentMentionClickSpan;
import awais.instagrabber.customviews.helpers.SwipeGestureListener;
import awais.instagrabber.databinding.ActivityViewerBinding;
import awais.instagrabber.interfaces.SwipeEvent;
import awais.instagrabber.models.BasePostModel;
import awais.instagrabber.models.PostModel;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.models.ViewerPostModel;
import awais.instagrabber.models.enums.DownloadMethod;
import awais.instagrabber.models.enums.ItemGetType;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
public final class PostViewer extends BaseLanguageActivity {
private ActivityViewerBinding viewerBinding;
private String url, prevUsername, commentsEndCursor;
private ProfileModel profileModel;
private BasePostModel postModel;
private ViewerPostModel viewerPostModel;
private SimpleExoPlayer player;
private ArrayAdapter<String> profileDialogAdapter;
private View viewsContainer, viewerCaptionParent;
private GestureDetectorCompat gestureDetector;
private SwipeEvent swipeEvent;
private CharSequence postCaption = null, postShortCode;
private Resources resources;
private boolean session = false, isFromShare;
private int slidePos = 0, lastSlidePos = 0;
private ItemGetType itemGetType;
@SuppressLint("ClickableViewAccessibility")
final View.OnTouchListener gestureTouchListener = new View.OnTouchListener() {
private float startX;
private float startY;
@Override
public boolean onTouch(final View v, final MotionEvent event) {
if (v == viewerCaptionParent) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = event.getX();
startY = event.getY();
break;
case MotionEvent.ACTION_UP:
if (!(Utils.isEmpty(postCaption) ||
Math.abs(startX - event.getX()) > 50 || Math.abs(startY - event.getY()) > 50)) {
Utils.copyText(PostViewer.this, postCaption);
return false;
}
}
}
return gestureDetector.onTouchEvent(event);
}
};
private final DialogInterface.OnClickListener profileDialogListener = (dialog, which) -> {
final String username = viewerPostModel.getUsername();
if (which == 0) {
searchUsername(username);
} else if (profileModel != null && which == 1) {
startActivity(new Intent(this, ProfileViewer.class)
.putExtra(Constants.EXTRAS_PROFILE, profileModel));
}
};
private final View.OnClickListener onClickListener = new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (v == viewerBinding.topPanel.ivProfilePic) {
new AlertDialog.Builder(PostViewer.this).setAdapter(profileDialogAdapter, profileDialogListener)
.setNeutralButton(R.string.cancel, null).setTitle(viewerPostModel.getUsername()).show();
} else if (v == viewerBinding.ivToggleFullScreen) {
toggleFullscreen();
final LinearLayout topPanelRoot = viewerBinding.topPanel.getRoot();
final int iconRes;
if (containerLayoutParams.height == 0) {
containerLayoutParams.height = LinearLayout.LayoutParams.MATCH_PARENT;
iconRes = R.drawable.ic_fullscreen_exit;
topPanelRoot.setVisibility(View.GONE);
viewerBinding.btnDownload.setVisibility(View.VISIBLE);
} else {
containerLayoutParams.height = 0;
iconRes = R.drawable.ic_fullscreen;
topPanelRoot.setVisibility(View.VISIBLE);
viewerBinding.btnDownload.setVisibility(View.GONE);
}
viewerBinding.ivToggleFullScreen.setImageResource(iconRes);
viewerBinding.container.setLayoutParams(containerLayoutParams);
} else if (v == viewerBinding.bottomPanel.btnMute) {
if (player != null) {
final float intVol = player.getVolume() == 0f ? 1f : 0f;
player.setVolume(intVol);
viewerBinding.bottomPanel.btnMute.setImageResource(intVol == 0f ? R.drawable.vol : R.drawable.mute);
Utils.sessionVolumeFull = intVol == 1f;
}
} else {
final Object tag = v.getTag();
if (tag instanceof ViewerPostModel) {
viewerPostModel = (ViewerPostModel) tag;
slidePos = Math.max(0, viewerPostModel.getPosition());
refreshPost();
}
}
}
};
private final View.OnClickListener downloadClickListener = v -> {
if (ContextCompat.checkSelfPermission(this, Utils.PERMS[0]) == PackageManager.PERMISSION_GRANTED)
showDownloadDialog();
else
ActivityCompat.requestPermissions(this, Utils.PERMS, 8020);
};
private final PostsMediaAdapter mediaAdapter = new PostsMediaAdapter(null, onClickListener);
private RequestManager glideRequestManager;
private LinearLayout.LayoutParams containerLayoutParams;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewerBinding = ActivityViewerBinding.inflate(getLayoutInflater());
setContentView(viewerBinding.getRoot());
glideRequestManager = Glide.with(this);
final Intent intent = getIntent();
if (intent == null || !intent.hasExtra(Constants.EXTRAS_POST)
|| (postModel = (PostModel) intent.getSerializableExtra(Constants.EXTRAS_POST)) == null) {
Utils.errorFinish(this);
return;
}
containerLayoutParams = (LinearLayout.LayoutParams) viewerBinding.container.getLayoutParams();
if (intent.hasExtra(Constants.EXTRAS_TYPE))
itemGetType = (ItemGetType) intent.getSerializableExtra(Constants.EXTRAS_TYPE);
resources = getResources();
final View viewStoryPost = findViewById(R.id.viewStoryPost);
if (viewStoryPost != null) viewStoryPost.setVisibility(View.GONE);
viewerBinding.topPanel.title.setMovementMethod(new LinkMovementMethod());
viewerBinding.topPanel.title.setMentionClickListener((view, text, isHashtag) ->
onClickListener.onClick(viewerBinding.topPanel.ivProfilePic));
viewerBinding.topPanel.ivProfilePic.setOnClickListener(onClickListener);
viewerBinding.ivToggleFullScreen.setOnClickListener(onClickListener);
viewerBinding.btnDownload.setOnClickListener(downloadClickListener);
profileDialogAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1,
new String[]{resources.getString(R.string.open_profile), resources.getString(R.string.view_pfp)});
postModel.setPosition(intent.getIntExtra(Constants.EXTRAS_INDEX, -1));
postShortCode = postModel.getShortCode();
final boolean postIdNull = postModel.getPostId() == null;
if (!postIdNull)
setupPostInfoBar(intent.getStringExtra(Constants.EXTRAS_USER), postModel.getItemType());
isFromShare = postModel.getPosition() == -1 || postIdNull;
viewerCaptionParent = (View) viewerBinding.bottomPanel.viewerCaption.getParent();
viewsContainer = (View) viewerBinding.bottomPanel.tvVideoViews.getParent();
viewerBinding.mediaList.setLayoutManager(new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false));
viewerBinding.mediaList.setAdapter(mediaAdapter);
viewerBinding.mediaList.setVisibility(View.GONE);
swipeEvent = isRight -> {
final List<? extends BasePostModel> itemGetterItems;
final boolean isMainSwipe;
if (itemGetType != null && Main.itemGetter != null) {
itemGetterItems = Main.itemGetter.get(itemGetType);
isMainSwipe = !(itemGetterItems.size() < 1 || itemGetType == ItemGetType.MAIN_ITEMS && isFromShare);
} else {
itemGetterItems = null;
isMainSwipe = false;
}
final BasePostModel[] basePostModels = mediaAdapter != null ? mediaAdapter.getPostModels() : null;
final int slides = basePostModels != null ? basePostModels.length : 0;
int position = postModel.getPosition();
if (isRight) {
--slidePos;
if (!isMainSwipe && slidePos < 0) slidePos = 0;
if (slides > 0 && slidePos >= 0) {
if (basePostModels[slidePos] instanceof ViewerPostModel) {
viewerPostModel = (ViewerPostModel) basePostModels[slidePos];
}
refreshPost();
return;
}
if (isMainSwipe && --position < 0) position = itemGetterItems.size() - 1;
} else {
++slidePos;
if (!isMainSwipe && slidePos >= slides) slidePos = slides - 1;
if (slides > 0 && slidePos < slides) {
if (basePostModels[slidePos] instanceof ViewerPostModel) {
viewerPostModel = (ViewerPostModel) basePostModels[slidePos];
}
refreshPost();
return;
}
if (isMainSwipe && ++position >= itemGetterItems.size()) position = 0;
}
if (isMainSwipe) {
slidePos = 0;
Log.d("AWAISKING_APP", "swipe left <<< post[" + position + "]: " + postModel + " -- " + slides);
postModel = itemGetterItems.get(position);
postModel.setPosition(position);
viewPost();
}
};
gestureDetector = new GestureDetectorCompat(this, new SwipeGestureListener(swipeEvent));
viewPost();
}
private void viewPost() {
lastSlidePos = 0;
mediaAdapter.setData(null);
viewsContainer.setVisibility(View.GONE);
viewerCaptionParent.setVisibility(View.GONE);
viewerBinding.mediaList.setVisibility(View.GONE);
viewerBinding.btnDownload.setVisibility(View.GONE);
viewerBinding.bottomPanel.btnMute.setVisibility(View.GONE);
viewerBinding.bottomPanel.tvPostDate.setVisibility(View.GONE);
viewerBinding.bottomPanel.btnComments.setVisibility(View.GONE);
viewerBinding.bottomPanel.btnDownload.setVisibility(View.INVISIBLE);
viewerBinding.bottomPanel.viewerCaption.setText(null);
viewerBinding.bottomPanel.viewerCaption.setMentionClickListener(null);
viewerBinding.playerView.setVisibility(View.GONE);
viewerBinding.playerView.setPlayer(null);
viewerBinding.imageViewer.setImageResource(0);
viewerBinding.imageViewer.setImageDrawable(null);
new PostFetcher(postModel.getShortCode(), result -> {
if (result == null || result.length < 1) {
Toast.makeText(this, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
return;
}
viewerPostModel = result[0];
commentsEndCursor = viewerPostModel.getCommentsEndCursor();
mediaAdapter.setData(result);
if (result.length > 1) {
viewerBinding.mediaList.setVisibility(View.VISIBLE);
}
viewerCaptionParent.setOnTouchListener(gestureTouchListener);
viewerBinding.playerView.setOnTouchListener(gestureTouchListener);
viewerBinding.imageViewer.setOnSingleFlingListener((e1, e2, velocityX, velocityY) -> {
final float diffX = e2.getX() - e1.getX();
if (Math.abs(diffX) > Math.abs(e2.getY() - e1.getY()) && Math.abs(diffX) > SwipeGestureListener.SWIPE_THRESHOLD
&& Math.abs(velocityX) > SwipeGestureListener.SWIPE_VELOCITY_THRESHOLD) {
swipeEvent.onSwipe(diffX > 0);
return true;
}
return false;
});
final long commentsCount = viewerPostModel.getCommentsCount();
viewerBinding.bottomPanel.commentsCount.setText(String.valueOf(commentsCount));
viewerBinding.bottomPanel.btnComments.setVisibility(View.VISIBLE);
if (commentsCount > 0) {
viewerBinding.bottomPanel.btnComments.setOnClickListener(v ->
startActivityForResult(new Intent(this, CommentsViewer.class)
.putExtra(Constants.EXTRAS_END_CURSOR, commentsEndCursor)
.putExtra(Constants.EXTRAS_SHORTCODE, postShortCode), 6969));
viewerBinding.bottomPanel.btnComments.setClickable(true);
viewerBinding.bottomPanel.btnComments.setEnabled(true);
} else {
viewerBinding.bottomPanel.btnComments.setOnClickListener(null);
viewerBinding.bottomPanel.btnComments.setClickable(false);
viewerBinding.bottomPanel.btnComments.setEnabled(false);
}
if (postModel instanceof PostModel) {
final PostModel postModel = (PostModel) this.postModel;
postModel.setPostId(viewerPostModel.getPostId());
postModel.setTimestamp(viewerPostModel.getTimestamp());
postModel.setPostCaption(viewerPostModel.getPostCaption());
}
setupPostInfoBar(viewerPostModel.getUsername(), viewerPostModel.getItemType());
postCaption = postModel.getPostCaption();
viewerCaptionParent.setVisibility(View.VISIBLE);
viewerBinding.bottomPanel.btnDownload.setOnClickListener(downloadClickListener);
refreshPost();
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void searchUsername(final String text) {
if (Main.scanHack != null) {
Main.scanHack.onResult(text);
finish();
}
}
private void setupVideo() {
viewerBinding.playerView.setVisibility(View.VISIBLE);
viewerBinding.bottomPanel.btnDownload.setVisibility(View.VISIBLE);
viewerBinding.bottomPanel.btnMute.setVisibility(View.VISIBLE);
viewsContainer.setVisibility(View.VISIBLE);
viewerBinding.progressView.setVisibility(View.GONE);
viewerBinding.imageViewer.setVisibility(View.GONE);
viewerBinding.imageViewer.setImageDrawable(null);
viewerBinding.bottomPanel.tvVideoViews.setText(String.valueOf(viewerPostModel.getVideoViews()));
player = new SimpleExoPlayer.Builder(this).build();
viewerBinding.playerView.setPlayer(player);
float vol = Utils.settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f;
if (vol == 0f && Utils.sessionVolumeFull) vol = 1f;
player.setVolume(vol);
player.setPlayWhenReady(Utils.settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS));
final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(new DefaultDataSourceFactory(this, "instagram"))
.createMediaSource(Uri.parse(url));
mediaSource.addEventListener(new Handler(), new MediaSourceEventListener() {
@Override
public void onLoadCompleted(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
viewerBinding.progressView.setVisibility(View.GONE);
}
@Override
public void onLoadStarted(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
viewerBinding.progressView.setVisibility(View.VISIBLE);
}
@Override
public void onLoadCanceled(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
viewerBinding.progressView.setVisibility(View.GONE);
}
@Override
public void onLoadError(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData, final IOException error, final boolean wasCanceled) {
viewerBinding.progressView.setVisibility(View.GONE);
}
});
player.prepare(mediaSource);
player.setVolume(vol);
viewerBinding.bottomPanel.btnMute.setImageResource(vol == 0f ? R.drawable.vol : R.drawable.mute);
viewerBinding.bottomPanel.btnMute.setOnClickListener(onClickListener);
}
private void setupImage() {
viewsContainer.setVisibility(View.GONE);
viewerBinding.playerView.setVisibility(View.GONE);
viewerBinding.progressView.setVisibility(View.VISIBLE);
viewerBinding.bottomPanel.btnMute.setVisibility(View.GONE);
viewerBinding.bottomPanel.btnDownload.setVisibility(View.VISIBLE);
viewerBinding.imageViewer.setImageDrawable(null);
viewerBinding.imageViewer.setVisibility(View.VISIBLE);
viewerBinding.imageViewer.setZoomable(true);
viewerBinding.imageViewer.setZoomTransitionDuration(420);
viewerBinding.imageViewer.setMaximumScale(7.2f);
glideRequestManager.load(url).listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target<Drawable> target, final boolean isFirstResource) {
viewerBinding.progressView.setVisibility(View.GONE);
return false;
}
@Override
public boolean onResourceReady(final Drawable resource, final Object model, final Target<Drawable> target, final DataSource dataSource, final boolean isFirstResource) {
viewerBinding.progressView.setVisibility(View.GONE);
return false;
}
}).into(viewerBinding.imageViewer);
}
private void showDownloadDialog() {
final ArrayList<BasePostModel> postModels = new ArrayList<>();
if (!session && viewerBinding.mediaList.getVisibility() == View.VISIBLE) {
final DialogInterface.OnClickListener clickListener = (dialog, which) -> {
postModels.clear();
if (which == DialogInterface.BUTTON_NEGATIVE) {
final BasePostModel[] adapterPostModels = mediaAdapter.getPostModels();
for (int i = 0, size = mediaAdapter.getItemCount(); i < size; ++i) {
if (adapterPostModels[i] instanceof ViewerPostModel)
postModels.add(adapterPostModels[i]);
}
} else if (which == DialogInterface.BUTTON_POSITIVE) {
postModels.add(viewerPostModel);
} else {
session = true;
postModels.add(viewerPostModel);
}
if (postModels.size() > 0)
Utils.batchDownload(this, viewerPostModel.getUsername(), DownloadMethod.DOWNLOAD_POST_VIEWER, postModels);
};
new AlertDialog.Builder(this).setTitle(R.string.post_viewer_download_dialog_title)
.setMessage(R.string.post_viewer_download_message)
.setNeutralButton(R.string.post_viewer_download_session, clickListener).setPositiveButton(R.string.post_viewer_download_current, clickListener)
.setNegativeButton(R.string.post_viewer_download_album, clickListener).show();
} else {
Utils.batchDownload(this, viewerPostModel.getUsername(), DownloadMethod.DOWNLOAD_POST_VIEWER, Collections.singletonList(viewerPostModel));
}
}
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 8020 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
showDownloadDialog();
}
@Override
protected void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == 6969) {
setResult(RESULT_OK);
finish();
}
}
@Override
public void onPause() {
super.onPause();
if (Build.VERSION.SDK_INT < 24) releasePlayer();
}
@Override
public void onStop() {
super.onStop();
if (Build.VERSION.SDK_INT >= 24) releasePlayer();
}
@Override
protected void onResume() {
super.onResume();
if (player == null && viewerPostModel != null && viewerPostModel.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO)
setupVideo();
else if (player != null) {
player.setPlayWhenReady(true);
player.getPlaybackState();
}
}
private void refreshPost() {
postShortCode = postModel.getShortCode();
if (viewerBinding.mediaList.getVisibility() == View.VISIBLE) {
ViewerPostModel item = mediaAdapter.getItemAt(lastSlidePos);
if (item != null) {
item.setCurrentSlide(false);
mediaAdapter.notifyItemChanged(lastSlidePos, item);
}
item = mediaAdapter.getItemAt(slidePos);
if (item != null) {
item.setCurrentSlide(true);
mediaAdapter.notifyItemChanged(slidePos, item);
}
}
lastSlidePos = slidePos;
postCaption = viewerPostModel.getPostCaption();
if (Utils.hasMentions(postCaption)) {
viewerBinding.bottomPanel.viewerCaption.setText(Utils.getMentionText(postCaption), TextView.BufferType.SPANNABLE);
viewerBinding.bottomPanel.viewerCaption.setMentionClickListener((view, text, isHashtag) ->
new AlertDialog.Builder(PostViewer.this).setTitle(text)
.setMessage(isHashtag ? R.string.comment_view_mention_hash_search : R.string.comment_view_mention_user_search)
.setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok,
(dialog, which) -> searchUsername(text)).show());
} else {
viewerBinding.bottomPanel.viewerCaption.setMentionClickListener(null);
viewerBinding.bottomPanel.viewerCaption.setText(postCaption);
}
setupPostInfoBar(viewerPostModel.getUsername(), viewerPostModel.getItemType());
if (postModel instanceof PostModel) {
final PostModel postModel = (PostModel) this.postModel;
postModel.setPostId(viewerPostModel.getPostId());
postModel.setTimestamp(viewerPostModel.getTimestamp());
postModel.setPostCaption(viewerPostModel.getPostCaption());
}
viewerBinding.bottomPanel.tvPostDate.setText(viewerPostModel.getPostDate());
viewerBinding.bottomPanel.tvPostDate.setVisibility(View.VISIBLE);
viewerBinding.bottomPanel.tvPostDate.setSelected(true);
url = viewerPostModel.getDisplayUrl();
releasePlayer();
viewerBinding.btnDownload.setVisibility(containerLayoutParams.height == 0 ? View.GONE : View.VISIBLE);
if (viewerPostModel.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) setupVideo();
else setupImage();
}
private void releasePlayer() {
if (player != null) {
player.release();
player = null;
}
}
private void setupPostInfoBar(final String from, final MediaItemType mediaItemType) {
if (prevUsername == null || !prevUsername.equals(from)) {
viewerBinding.topPanel.ivProfilePic.setImageBitmap(null);
viewerBinding.topPanel.ivProfilePic.setImageDrawable(null);
viewerBinding.topPanel.ivProfilePic.setImageResource(0);
if (from.charAt(0) != '#')
new ProfileFetcher(from, result -> {
profileModel = result;
if (result != null) {
final String hdProfilePic = result.getHdProfilePic();
final String sdProfilePic = result.getSdProfilePic();
final boolean hdPicEmpty = Utils.isEmpty(hdProfilePic);
glideRequestManager.load(hdPicEmpty ? sdProfilePic : hdProfilePic).listener(new RequestListener<Drawable>() {
private boolean loaded = true;
@Override
public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target<Drawable> target, final boolean isFirstResource) {
viewerBinding.topPanel.ivProfilePic.setEnabled(false);
viewerBinding.topPanel.ivProfilePic.setOnClickListener(null);
if (loaded) {
loaded = false;
if (!Utils.isEmpty(sdProfilePic)) glideRequestManager.load(sdProfilePic).listener(this)
.into(viewerBinding.topPanel.ivProfilePic);
}
return false;
}
@Override
public boolean onResourceReady(final Drawable resource, final Object model, final Target<Drawable> target, final DataSource dataSource, final boolean isFirstResource) {
viewerBinding.topPanel.ivProfilePic.setEnabled(true);
viewerBinding.topPanel.ivProfilePic.setOnClickListener(onClickListener);
return false;
}
}).into(viewerBinding.topPanel.ivProfilePic);
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
prevUsername = from;
}
final String titlePrefix = resources.getString(mediaItemType == MediaItemType.MEDIA_TYPE_VIDEO ?
R.string.post_viewer_video_post : R.string.post_viewer_image_post);
if (Utils.isEmpty(from)) viewerBinding.topPanel.title.setText(titlePrefix);
else {
final CharSequence titleText = resources.getString(R.string.post_viewer_post_from, titlePrefix, from) + " ";
final int titleLen = titleText.length();
final SpannableString spannableString = new SpannableString(titleText);
spannableString.setSpan(new CommentMentionClickSpan(), titleLen - from.length() - 1, titleLen - 1, 0);
viewerBinding.topPanel.title.setText(spannableString);
}
}
private void toggleFullscreen() {
final View decorView = getWindow().getDecorView();
int newUiOptions = decorView.getSystemUiVisibility();
newUiOptions ^= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
newUiOptions ^= View.SYSTEM_UI_FLAG_FULLSCREEN;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
newUiOptions ^= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
decorView.setSystemUiVisibility(newUiOptions);
}
}

215
app/src/main/java/awais/instagrabber/activities/ProfileViewer.java

@ -0,0 +1,215 @@
package awais.instagrabber.activities;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import java.io.File;
import awais.instagrabber.R;
import awais.instagrabber.asyncs.DownloadAsync;
import awais.instagrabber.asyncs.ProfilePictureFetcher;
import awais.instagrabber.databinding.ActivityProfileBinding;
import awais.instagrabber.dialogs.ProfileSettingsDialog;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.models.enums.ProfilePictureFetchMode;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Constants.PROFILE_FETCH_MODE;
public final class ProfileViewer extends BaseLanguageActivity {
private final ProfilePictureFetchMode[] fetchModes = {
ProfilePictureFetchMode.INSTADP,
ProfilePictureFetchMode.INSTA_STALKER,
ProfilePictureFetchMode.INSTAFULLSIZE,
};
private ActivityProfileBinding profileBinding;
private ProfileModel profileModel;
private MenuItem menuItemDownload;
private String profilePicUrl;
private FragmentManager fragmentManager;
private FetchListener<String> fetchListener;
private boolean errorHandled = false;
private boolean fallbackToProfile = false;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
profileBinding = ActivityProfileBinding.inflate(getLayoutInflater());
setContentView(profileBinding.getRoot());
setSupportActionBar(profileBinding.toolbar.toolbar);
final Intent intent = getIntent();
if (intent == null || !intent.hasExtra(Constants.EXTRAS_PROFILE)
|| (profileModel = (ProfileModel) intent.getSerializableExtra(Constants.EXTRAS_PROFILE)) == null) {
Utils.errorFinish(this);
return;
}
fragmentManager = getSupportFragmentManager();
final String id = profileModel.getId();
final String username = profileModel.getUsername();
profileBinding.toolbar.toolbar.setTitle(username);
profileBinding.progressView.setVisibility(View.VISIBLE);
profileBinding.imageViewer.setVisibility(View.VISIBLE);
profileBinding.imageViewer.setZoomable(true);
profileBinding.imageViewer.setZoomTransitionDuration(420);
profileBinding.imageViewer.setMaximumScale(7.2f);
final int fetchIndex = Math.min(2, Math.max(0, Utils.settingsHelper.getInteger(PROFILE_FETCH_MODE)));
final ProfilePictureFetchMode fetchMode = fetchModes[fetchIndex];
fetchListener = profileUrl -> {
profilePicUrl = profileUrl;
if (!fallbackToProfile && Utils.isEmpty(profilePicUrl)) {
fallbackToProfile = true;
new ProfilePictureFetcher(username, id, fetchListener, fetchMode).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return;
}
if (errorHandled && fallbackToProfile || Utils.isEmpty(profilePicUrl))
profilePicUrl = profileModel.getHdProfilePic();
final RequestManager glideRequestManager = Glide.with(this);
glideRequestManager.load(profilePicUrl).addListener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target<Drawable> target, final boolean isFirstResource) {
fallbackToProfile = true;
if (!errorHandled) {
errorHandled = true;
new ProfilePictureFetcher(username, id, fetchListener, fetchModes[Math.min(2, Math.max(0, fetchIndex + 1))])
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
glideRequestManager.load(profileModel.getHdProfilePic()).into(profileBinding.imageViewer);
showImageInfo();
}
profileBinding.progressView.setVisibility(View.GONE);
return false;
}
@Override
public boolean onResourceReady(final Drawable resource, final Object model, final Target<Drawable> target, final DataSource dataSource, final boolean isFirstResource) {
if (menuItemDownload != null) menuItemDownload.setEnabled(true);
showImageInfo();
profileBinding.progressView.setVisibility(View.GONE);
return false;
}
private void showImageInfo() {
final Drawable drawable = profileBinding.imageViewer.getDrawable();
if (drawable != null) {
final StringBuilder info = new StringBuilder(getString(R.string.profile_viewer_imageinfo, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()));
if (drawable instanceof BitmapDrawable) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
final String colorDepthPrefix = getString(R.string.profile_viewer_colordepth_prefix);
switch (bitmap.getConfig()) {
case ALPHA_8:
info.append(colorDepthPrefix).append(" 8-bits(A)");
break;
case RGB_565:
info.append(colorDepthPrefix).append(" 16-bits-A");
break;
case ARGB_4444:
info.append(colorDepthPrefix).append(" 16-bits+A");
break;
case ARGB_8888:
info.append(colorDepthPrefix).append(" 32-bits+A");
break;
case RGBA_F16:
info.append(colorDepthPrefix).append(" 64-bits+A");
break;
case HARDWARE:
info.append(colorDepthPrefix).append(" auto");
break;
}
}
}
profileBinding.imageInfo.setText(info);
profileBinding.imageInfo.setVisibility(View.VISIBLE);
}
}
}).into(profileBinding.imageViewer);
};
new ProfilePictureFetcher(username, id, fetchListener, fetchMode).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void downloadProfilePicture() {
int error = 0;
if (profileModel != null) {
final File dir = new File(Environment.getExternalStorageDirectory(), "Download");
if (dir.exists() || dir.mkdirs()) {
final File saveFile = new File(dir, profileModel.getUsername() + '_' + System.currentTimeMillis()
+ Utils.getExtensionFromModel(profilePicUrl, profileModel));
new DownloadAsync(this,
profilePicUrl,
saveFile,
result -> {
final int toastRes = result != null && result.exists() ?
R.string.downloader_downloaded_in_folder : R.string.downloader_error_download_file;
Toast.makeText(this, toastRes, Toast.LENGTH_SHORT).show();
}).setItems(null, profileModel.getUsername()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else error = 1;
} else error = 2;
if (error == 1) Toast.makeText(this, R.string.downloader_error_creating_folder, Toast.LENGTH_SHORT).show();
else if (error == 2) Toast.makeText(this, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
final MenuItem.OnMenuItemClickListener menuItemClickListener = item -> {
if (item == menuItemDownload) {
downloadProfilePicture();
} else {
new ProfileSettingsDialog().show(fragmentManager, "settings");
}
return true;
};
menu.findItem(R.id.action_search).setVisible(false);
menuItemDownload = menu.findItem(R.id.action_download);
menuItemDownload.setVisible(true);
menuItemDownload.setEnabled(false);
menuItemDownload.setOnMenuItemClickListener(menuItemClickListener);
final MenuItem menuItemSettings = menu.findItem(R.id.action_settings);
menuItemSettings.setVisible(true);
menuItemSettings.setOnMenuItemClickListener(menuItemClickListener);
return true;
}
}

354
app/src/main/java/awais/instagrabber/activities/StoryViewer.java

@ -0,0 +1,354 @@
package awais.instagrabber.activities;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import java.io.File;
import java.io.IOException;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.R;
import awais.instagrabber.adapters.StoriesAdapter;
import awais.instagrabber.asyncs.DownloadAsync;
import awais.instagrabber.customviews.helpers.SwipeGestureListener;
import awais.instagrabber.databinding.ActivityStoryViewerBinding;
import awais.instagrabber.interfaces.SwipeEvent;
import awais.instagrabber.models.PostModel;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import static awais.instagrabber.customviews.helpers.SwipeGestureListener.SWIPE_THRESHOLD;
import static awais.instagrabber.customviews.helpers.SwipeGestureListener.SWIPE_VELOCITY_THRESHOLD;
import static awais.instagrabber.utils.Constants.FOLDER_PATH;
import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO;
import static awais.instagrabber.utils.Utils.logCollector;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class StoryViewer extends BaseLanguageActivity {
private final StoriesAdapter storiesAdapter = new StoriesAdapter(null, new View.OnClickListener() {
@Override
public void onClick(final View v) {
final Object tag = v.getTag();
if (tag instanceof StoryModel) {
currentStory = (StoryModel) tag;
slidePos = currentStory.getPosition();
refreshStory();
}
}
});
private ActivityStoryViewerBinding storyViewerBinding;
private StoryModel[] storyModels;
private GestureDetectorCompat gestureDetector;
private SimpleExoPlayer player;
private SwipeEvent swipeEvent;
private MenuItem menuDownload;
private StoryModel currentStory;
private String url, username;
private int slidePos = 0, lastSlidePos = 0;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
storyViewerBinding = ActivityStoryViewerBinding.inflate(getLayoutInflater());
setContentView(storyViewerBinding.getRoot());
setSupportActionBar(storyViewerBinding.toolbar.toolbar);
final Intent intent = getIntent();
if (intent == null || !intent.hasExtra(Constants.EXTRAS_STORIES)
|| (storyModels = (StoryModel[]) intent.getSerializableExtra(Constants.EXTRAS_STORIES)) == null) {
Utils.errorFinish(this);
return;
}
username = intent.getStringExtra(Constants.EXTRAS_USERNAME);
final String highlight = intent.getStringExtra(Constants.EXTRAS_HIGHLIGHT);
final boolean hasUsername = !Utils.isEmpty(username);
final boolean hasHighlight = !Utils.isEmpty(highlight);
if (hasUsername) {
storyViewerBinding.toolbar.toolbar.setTitle(username);
if (hasHighlight) storyViewerBinding.toolbar.toolbar.setSubtitle(getString(R.string.title_highlight, highlight));
else storyViewerBinding.toolbar.toolbar.setSubtitle(R.string.title_user_story);
}
storyViewerBinding.storiesList.setVisibility(View.GONE);
storyViewerBinding.storiesList.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
storyViewerBinding.storiesList.setAdapter(storiesAdapter);
swipeEvent = new SwipeEvent() {
private final int storiesLen = storyModels != null ? storyModels.length : 0;
@Override
public void onSwipe(final boolean isRightSwipe) {
if (storyModels != null && storiesLen > 0) {
if (isRightSwipe) {
if (--slidePos <= 0) slidePos = 0;
} else if (++slidePos >= storiesLen) slidePos = storiesLen - 1;
currentStory = storyModels[slidePos];
slidePos = currentStory.getPosition();
refreshStory();
}
}
};
gestureDetector = new GestureDetectorCompat(this, new SwipeGestureListener(swipeEvent));
viewPost();
}
@SuppressLint("ClickableViewAccessibility")
private void viewPost() {
lastSlidePos = 0;
storyViewerBinding.storiesList.setVisibility(View.GONE);
storiesAdapter.setData(null);
if (menuDownload != null) menuDownload.setVisible(false);
storyViewerBinding.playerView.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
storyViewerBinding.imageViewer.setOnSingleFlingListener((e1, e2, velocityX, velocityY) -> {
final float diffX = e2.getX() - e1.getX();
try {
if (Math.abs(diffX) > Math.abs(e2.getY() - e1.getY()) && Math.abs(diffX) > SWIPE_THRESHOLD
&& Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
swipeEvent.onSwipe(diffX > 0);
return true;
}
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ACTIVITY_STORY_VIEWER, "viewPost",
new Pair<>("swipeEvent", swipeEvent),
new Pair<>("diffX", diffX));
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return false;
});
storyViewerBinding.viewStoryPost.setOnClickListener(v -> {
final Object tag = v.getTag();
if (tag instanceof CharSequence) startActivity(new Intent(this, PostViewer.class)
.putExtra(Constants.EXTRAS_POST, new PostModel(tag.toString())));
});
storiesAdapter.setData(storyModels);
if (storyModels.length > 1) storyViewerBinding.storiesList.setVisibility(View.VISIBLE);
currentStory = storyModels[0];
refreshStory();
}
private void setupVideo() {
storyViewerBinding.playerView.setVisibility(View.VISIBLE);
storyViewerBinding.progressView.setVisibility(View.GONE);
storyViewerBinding.imageViewer.setVisibility(View.GONE);
storyViewerBinding.imageViewer.setImageDrawable(null);
player = new SimpleExoPlayer.Builder(this).build();
storyViewerBinding.playerView.setPlayer(player);
player.setPlayWhenReady(settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS));
final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(new DefaultDataSourceFactory(this, "instagram"))
.createMediaSource(Uri.parse(url));
mediaSource.addEventListener(new Handler(), new MediaSourceEventListener() {
@Override
public void onLoadCompleted(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
if (menuDownload != null) menuDownload.setVisible(true);
storyViewerBinding.progressView.setVisibility(View.GONE);
}
@Override
public void onLoadStarted(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
if (menuDownload != null) menuDownload.setVisible(true);
storyViewerBinding.progressView.setVisibility(View.VISIBLE);
}
@Override
public void onLoadCanceled(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
storyViewerBinding.progressView.setVisibility(View.GONE);
}
@Override
public void onLoadError(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData, final IOException error, final boolean wasCanceled) {
if (menuDownload != null) menuDownload.setVisible(false);
storyViewerBinding.progressView.setVisibility(View.GONE);
}
});
player.prepare(mediaSource);
storyViewerBinding.playerView.setOnClickListener(v -> {
if (player != null) {
if (player.getPlaybackState() == Player.STATE_ENDED) player.seekTo(0);
player.setPlayWhenReady(player.getPlaybackState() == Player.STATE_ENDED || !player.isPlaying());
}
});
}
private void setupImage() {
storyViewerBinding.progressView.setVisibility(View.VISIBLE);
storyViewerBinding.playerView.setVisibility(View.GONE);
storyViewerBinding.imageViewer.setImageDrawable(null);
storyViewerBinding.imageViewer.setVisibility(View.VISIBLE);
storyViewerBinding.imageViewer.setZoomable(true);
storyViewerBinding.imageViewer.setZoomTransitionDuration(420);
storyViewerBinding.imageViewer.setMaximumScale(7.2f);
Glide.with(this).load(url).listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target<Drawable> target, final boolean isFirstResource) {
storyViewerBinding.progressView.setVisibility(View.GONE);
return false;
}
@Override
public boolean onResourceReady(final Drawable resource, final Object model, final Target<Drawable> target, final DataSource dataSource, final boolean isFirstResource) {
if (menuDownload != null) menuDownload.setVisible(true);
storyViewerBinding.progressView.setVisibility(View.GONE);
return false;
}
}).into(storyViewerBinding.imageViewer);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
menu.findItem(R.id.action_settings).setVisible(false);
menu.findItem(R.id.action_search).setVisible(false);
menuDownload = menu.findItem(R.id.action_download);
menuDownload.setVisible(true);
menuDownload.setOnMenuItemClickListener(item -> {
if (ContextCompat.checkSelfPermission(this, Utils.PERMS[0]) == PackageManager.PERMISSION_GRANTED)
downloadStory();
else
ActivityCompat.requestPermissions(this, Utils.PERMS, 8020);
return true;
});
return true;
}
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 8020 && grantResults[0] == PackageManager.PERMISSION_GRANTED) downloadStory();
}
@Override
public void onPause() {
super.onPause();
if (Build.VERSION.SDK_INT < 24) releasePlayer();
}
@Override
public void onStop() {
super.onStop();
if (Build.VERSION.SDK_INT >= 24) releasePlayer();
}
private void downloadStory() {
int error = 0;
if (currentStory != null) {
File dir = new File(Environment.getExternalStorageDirectory(), "Download");
if (settingsHelper.getBoolean(FOLDER_SAVE_TO)) {
final String customPath = settingsHelper.getString(FOLDER_PATH);
if (!Utils.isEmpty(customPath)) dir = new File(customPath);
}
if (settingsHelper.getBoolean(Constants.DOWNLOAD_USER_FOLDER) && !Utils.isEmpty(username))
dir = new File(dir, username);
if (dir.exists() || dir.mkdirs()) {
final String storyUrl = currentStory.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO ? currentStory.getVideoUrl() : currentStory.getStoryUrl();
final File saveFile = new File(dir, currentStory.getStoryMediaId() + "_" + currentStory.getTimestamp()
+ Utils.getExtensionFromModel(storyUrl, currentStory));
new DownloadAsync(this, storyUrl, saveFile, result -> {
final int toastRes = result != null && result.exists() ? R.string.downloader_complete
: R.string.downloader_error_download_file;
Toast.makeText(this, toastRes, Toast.LENGTH_SHORT).show();
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else error = 1;
} else error = 2;
if (error == 1) Toast.makeText(this, R.string.downloader_error_creating_folder, Toast.LENGTH_SHORT).show();
else if (error == 2) Toast.makeText(this, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
}
private void refreshStory() {
if (storyViewerBinding.storiesList.getVisibility() == View.VISIBLE) {
StoryModel item = storiesAdapter.getItemAt(lastSlidePos);
if (item != null) {
item.setCurrentSlide(false);
storiesAdapter.notifyItemChanged(lastSlidePos, item);
}
item = storiesAdapter.getItemAt(slidePos);
if (item != null) {
item.setCurrentSlide(true);
storiesAdapter.notifyItemChanged(slidePos, item);
}
}
lastSlidePos = slidePos;
final MediaItemType itemType = currentStory.getItemType();
if (menuDownload != null) menuDownload.setVisible(false);
url = itemType == MediaItemType.MEDIA_TYPE_VIDEO ? currentStory.getVideoUrl() : currentStory.getStoryUrl();
final String shortCode = currentStory.getTappableShortCode();
storyViewerBinding.viewStoryPost.setVisibility(shortCode != null ? View.VISIBLE : View.GONE);
storyViewerBinding.viewStoryPost.setTag(shortCode);
releasePlayer();
if (itemType == MediaItemType.MEDIA_TYPE_VIDEO) setupVideo();
else setupImage();
}
private void releasePlayer() {
if (player != null) {
try { player.stop(true); } catch (Exception ignored) { }
try { player.release(); } catch (Exception ignored) { }
player = null;
}
}
}

136
app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java

@ -0,0 +1,136 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import java.util.ArrayList;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.CommentViewHolder;
import awais.instagrabber.interfaces.MentionClickListener;
import awais.instagrabber.models.CommentModel;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.utils.LocaleUtils;
import awais.instagrabber.utils.Utils;
public final class CommentsAdapter extends RecyclerView.Adapter<CommentViewHolder> implements Filterable {
private final boolean isParent;
private final Filter filter = new Filter() {
@NonNull
@Override
protected FilterResults performFiltering(final CharSequence filter) {
final FilterResults results = new FilterResults();
results.values = commentModels;
final int commentsLen = commentModels == null ? 0 : commentModels.length;
if (commentModels != null && commentsLen > 0 && !Utils.isEmpty(filter)) {
final String query = filter.toString().toLowerCase();
final ArrayList<CommentModel> filterList = new ArrayList<>(commentsLen);
for (final CommentModel commentModel : commentModels) {
final String commentText = commentModel.getText().toString().toLowerCase();
if (commentText.contains(query)) filterList.add(commentModel);
else {
final CommentModel[] childCommentModels = commentModel.getChildCommentModels();
if (childCommentModels != null) {
for (final CommentModel childCommentModel : childCommentModels) {
final String childCommentText = childCommentModel.getText().toString().toLowerCase();
if (childCommentText.contains(query)) filterList.add(commentModel);
}
}
}
}
filterList.trimToSize();
results.values = filterList.toArray(new CommentModel[0]);
}
return results;
}
@Override
protected void publishResults(final CharSequence constraint, @NonNull final FilterResults results) {
if (results.values instanceof CommentModel[]) {
filteredCommentModels = (CommentModel[]) results.values;
notifyDataSetChanged();
}
}
};
private final View.OnClickListener onClickListener;
private final MentionClickListener mentionClickListener;
private final CommentModel[] commentModels;
private final String[] quantityStrings = new String[2];
private LayoutInflater layoutInflater;
private CommentModel[] filteredCommentModels;
public CommentsAdapter(final CommentModel[] commentModels, final boolean isParent, final View.OnClickListener onClickListener,
final MentionClickListener mentionClickListener) {
this.commentModels = this.filteredCommentModels = commentModels;
this.isParent = isParent;
this.onClickListener = onClickListener;
this.mentionClickListener = mentionClickListener;
}
@Override
public Filter getFilter() {
return filter;
}
@NonNull
@Override
public CommentViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final Context context = parent.getContext();
if (quantityStrings[0] == null) quantityStrings[0] = context.getString(R.string.single_like);
if (quantityStrings[1] == null) quantityStrings[1] = context.getString(R.string.multiple_likes);
if (layoutInflater == null) layoutInflater = LayoutInflater.from(context);
return new CommentViewHolder(layoutInflater.inflate(
isParent ? R.layout.item_comment // parent
: R.layout.item_comment_small, // child
parent, false), onClickListener, mentionClickListener);
}
@Override
public void onBindViewHolder(@NonNull final CommentViewHolder holder, final int position) {
final CommentModel commentModel = filteredCommentModels[position];
if (commentModel != null) {
holder.setCommentModel(commentModel);
holder.setCommment(commentModel.getText());
holder.setDate(commentModel.getDateTime());
final long likes = commentModel.getLikes();
holder.setLikes(String.format(LocaleUtils.getCurrentLocale(), "%d %s", likes, quantityStrings[likes == 1 ? 0 : 1]));
final ProfileModel profileModel = commentModel.getProfileModel();
if (profileModel != null) {
holder.setUsername(profileModel.getUsername());
Glide.with(layoutInflater.getContext())
.applyDefaultRequestOptions(new RequestOptions().skipMemoryCache(true))
.load(profileModel.getSdProfilePic()).into(holder.getProfilePicView());
}
if (holder.isParent()) {
final CommentModel[] childCommentModels = commentModel.getChildCommentModels();
if (childCommentModels != null && childCommentModels.length > 0)
holder.setChildAdapter(new CommentsAdapter(childCommentModels, false, onClickListener, mentionClickListener));
else holder.hideChildComments();
}
}
}
@Override
public int getItemCount() {
return filteredCommentModels == null ? 0 : filteredCommentModels.length;
}
}

116
app/src/main/java/awais/instagrabber/adapters/DirectMessagesAdapter.java

@ -0,0 +1,116 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import java.util.ArrayList;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.DirectMessageViewHolder;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.models.direct_messages.DirectItemModel;
import awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemActionLogModel;
import awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemReelShareModel;
import awais.instagrabber.models.direct_messages.InboxThreadModel;
import awais.instagrabber.models.enums.DirectItemType;
public final class DirectMessagesAdapter extends RecyclerView.Adapter<DirectMessageViewHolder> {
private final ArrayList<InboxThreadModel> inboxThreadModels;
private final View.OnClickListener onClickListener;
private LayoutInflater layoutInflater;
public DirectMessagesAdapter(final ArrayList<InboxThreadModel> inboxThreadModels, final View.OnClickListener onClickListener) {
this.inboxThreadModels = inboxThreadModels;
this.onClickListener = onClickListener;
}
@NonNull
@Override
public DirectMessageViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
if (layoutInflater == null) layoutInflater = LayoutInflater.from(parent.getContext());
return new DirectMessageViewHolder(layoutInflater.inflate(R.layout.layout_include_simple_item, parent, false),
onClickListener);
}
@Override
public void onBindViewHolder(@NonNull final DirectMessageViewHolder holder, final int position) {
final InboxThreadModel threadModel = inboxThreadModels.get(position);
final DirectItemModel[] itemModels;
holder.itemView.setTag(threadModel);
final RequestManager glideRequestManager = Glide.with(holder.itemView);
if (threadModel != null && (itemModels = threadModel.getItems()) != null) {
final ProfileModel[] users = threadModel.getUsers();
if (users.length > 1) {
holder.ivProfilePic.setVisibility(View.GONE);
holder.multipleProfilePicsContainer.setVisibility(View.VISIBLE);
for (int i = 0; i < Math.min(3, users.length); ++i)
glideRequestManager.load(users[i].getSdProfilePic()).into(holder.multipleProfilePics[i]);
} else {
holder.ivProfilePic.setVisibility(View.VISIBLE);
holder.multipleProfilePicsContainer.setVisibility(View.GONE);
glideRequestManager.load(users[0].getSdProfilePic()).into(holder.ivProfilePic);
}
holder.tvUsername.setText(threadModel.getThreadTitle());
final DirectItemModel lastItemModel = itemModels[itemModels.length - 1];
final DirectItemType itemType = lastItemModel.getItemType();
holder.notTextType.setVisibility(itemType != DirectItemType.TEXT ? View.VISIBLE : View.GONE);
final Context context = layoutInflater.getContext();
final CharSequence messageText;
if (itemType == DirectItemType.TEXT)
messageText = lastItemModel.getText();
else if (itemType == DirectItemType.LINK)
messageText = context.getString(R.string.direct_messages_sent_link);
else if (itemType == DirectItemType.MEDIA || itemType == DirectItemType.MEDIA_SHARE)
messageText = context.getString(R.string.direct_messages_sent_media);
else if (itemType == DirectItemType.ACTION_LOG) {
final DirectItemActionLogModel logModel = lastItemModel.getActionLogModel();
messageText = logModel != null ? logModel.getDescription() : "...";
} else if (itemType == DirectItemType.REEL_SHARE) {
final DirectItemReelShareModel reelShare = lastItemModel.getReelShare();
if (reelShare == null)
messageText = context.getString(R.string.direct_messages_sent_media);
else {
final String reelType = reelShare.getType();
final int textRes;
if ("reply".equals(reelType)) textRes = R.string.direct_messages_replied_story;
else if ("mention".equals(reelType)) textRes = R.string.direct_messages_mention_story;
else if ("reaction".equals(reelType)) textRes = R.string.direct_messages_reacted_story;
else textRes = R.string.direct_messages_sent_media;
messageText = context.getString(textRes) + " : " + reelShare.getText();
}
} else messageText = null;
holder.tvMessage.setText(messageText);
holder.tvDate.setText(lastItemModel.getDateTime());
}
}
@Override
public int getItemCount() {
return inboxThreadModels == null ? 0 : inboxThreadModels.size();
}
}

87
app/src/main/java/awais/instagrabber/adapters/DiscoverAdapter.java

@ -0,0 +1,87 @@
package awais.instagrabber.adapters;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import java.util.ArrayList;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.DiscoverViewHolder;
import awais.instagrabber.models.DiscoverItemModel;
import awais.instagrabber.models.enums.MediaItemType;
public final class DiscoverAdapter extends RecyclerView.Adapter<DiscoverViewHolder> {
private final ArrayList<DiscoverItemModel> discoverItemModels;
private final View.OnClickListener clickListener;
private final View.OnLongClickListener longClickListener;
private LayoutInflater layoutInflater;
public boolean isSelecting = false;
public DiscoverAdapter(final ArrayList<DiscoverItemModel> discoverItemModels, final View.OnClickListener clickListener,
final View.OnLongClickListener longClickListener) {
this.discoverItemModels = discoverItemModels;
this.longClickListener = longClickListener;
this.clickListener = clickListener;
}
@NonNull
@Override
public DiscoverViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
if (layoutInflater == null) layoutInflater = LayoutInflater.from(parent.getContext());
return new DiscoverViewHolder(layoutInflater.inflate(R.layout.item_post, parent, false));
}
@Override
public void onBindViewHolder(@NonNull final DiscoverViewHolder holder, final int position) {
final DiscoverItemModel itemModel = discoverItemModels.get(position);
if (itemModel != null) {
itemModel.setPosition(position);
holder.itemView.setTag(itemModel);
holder.itemView.setOnClickListener(clickListener);
holder.itemView.setOnLongClickListener(longClickListener);
final MediaItemType mediaType = itemModel.getItemType();
holder.typeIcon.setVisibility(mediaType == MediaItemType.MEDIA_TYPE_VIDEO || mediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE : View.GONE);
holder.typeIcon.setImageResource(mediaType == MediaItemType.MEDIA_TYPE_SLIDER ? R.drawable.slider : R.drawable.video);
holder.selectedView.setVisibility(itemModel.isSelected() ? View.VISIBLE : View.GONE);
holder.progressView.setVisibility(View.VISIBLE);
Glide.with(layoutInflater.getContext()).load(itemModel.getDisplayUrl()).listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target<Drawable> target, final boolean isFirstResource) {
holder.progressView.setVisibility(View.GONE);
return false;
}
@Override
public boolean onResourceReady(final Drawable resource, final Object model, final Target<Drawable> target, final DataSource dataSource, final boolean isFirstResource) {
holder.progressView.setVisibility(View.GONE);
return false;
}
}).into(holder.postImage);
}
}
@Override
public int getItemCount() {
return discoverItemModels == null ? 0 : discoverItemModels.size();
}
}

486
app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java

@ -0,0 +1,486 @@
package awais.instagrabber.adapters;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.StyleSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import com.github.chrisbanes.photoview.PhotoView;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import java.util.ArrayList;
import java.util.Collections;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.R;
import awais.instagrabber.activities.CommentsViewer;
import awais.instagrabber.activities.PostViewer;
import awais.instagrabber.adapters.viewholder.FeedItemViewHolder;
import awais.instagrabber.customviews.CommentMentionClickSpan;
import awais.instagrabber.customviews.RamboTextView;
import awais.instagrabber.interfaces.MentionClickListener;
import awais.instagrabber.models.BasePostModel;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.PostModel;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.models.ViewerPostModel;
import awais.instagrabber.models.enums.DownloadMethod;
import awais.instagrabber.models.enums.ItemGetType;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class FeedAdapter extends RecyclerView.Adapter<FeedItemViewHolder> {
private final static String ellipsize = "… more";
private final Activity activity;
private final LayoutInflater layoutInflater;
private final ArrayList<FeedModel> feedModels;
private final MentionClickListener mentionClickListener;
private final View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(@NonNull final View v) {
final Object tag = v.getTag();
if (tag instanceof FeedModel) {
final FeedModel feedModel = (FeedModel) tag;
if (v instanceof RamboTextView) {
if (feedModel.isMentionClicked())
feedModel.toggleCaption();
feedModel.setMentionClicked(false);
if (!expandCollapseTextView((RamboTextView) v, feedModel))
feedModel.toggleCaption();
} else {
final int id = v.getId();
switch (id) {
case R.id.btnComments:
activity.startActivityForResult(new Intent(activity, CommentsViewer.class)
.putExtra(Constants.EXTRAS_SHORTCODE, feedModel.getShortCode()), 6969);
break;
case R.id.viewStoryPost:
activity.startActivity(new Intent(activity, PostViewer.class)
.putExtra(Constants.EXTRAS_INDEX, feedModel.getPosition())
.putExtra(Constants.EXTRAS_POST, new PostModel(feedModel.getShortCode()))
.putExtra(Constants.EXTRAS_TYPE, ItemGetType.FEED_ITEMS));
break;
case R.id.btnDownload:
final Context context = v.getContext();
ProfileModel profileModel = feedModel.getProfileModel();
final String username = profileModel != null ? profileModel.getUsername() : null;
final ViewerPostModel[] sliderItems = feedModel.getSliderItems();
if (feedModel.getItemType() != MediaItemType.MEDIA_TYPE_SLIDER || sliderItems == null || sliderItems.length == 1)
Utils.batchDownload(context, username, DownloadMethod.DOWNLOAD_FEED, Collections.singletonList(feedModel));
else {
final ArrayList<BasePostModel> postModels = new ArrayList<>();
final DialogInterface.OnClickListener clickListener = (dialog, which) -> {
postModels.clear();
final boolean breakWhenFoundSelected = which == DialogInterface.BUTTON_POSITIVE;
for (final ViewerPostModel sliderItem : sliderItems) {
if (sliderItem != null) {
if (!breakWhenFoundSelected) postModels.add(sliderItem);
else if (sliderItem.isSelected()) {
postModels.add(sliderItem);
break;
}
}
}
// shows 0 items on first item of viewpager cause onPageSelected hasn't been called yet
if (breakWhenFoundSelected && postModels.size() == 0)
postModels.add(sliderItems[0]);
if (postModels.size() > 0)
Utils.batchDownload(context, username, DownloadMethod.DOWNLOAD_FEED, postModels);
};
new AlertDialog.Builder(context).setTitle(R.string.post_viewer_download_dialog_title)
.setPositiveButton(R.string.post_viewer_download_current, clickListener)
.setNegativeButton(R.string.post_viewer_download_album, clickListener).show();
}
break;
case R.id.ivProfilePic:
if (mentionClickListener != null) {
profileModel = feedModel.getProfileModel();
if (profileModel != null)
mentionClickListener.onClick(null, profileModel.getUsername(), false);
}
break;
}
}
}
}
};
private final View.OnLongClickListener longClickListener = v -> {
final Object tag;
if (v instanceof RamboTextView && (tag = v.getTag()) instanceof FeedModel)
Utils.copyText(v.getContext(), ((FeedModel) tag).getPostCaption());
return true;
};
public SimpleExoPlayer pagerPlayer;
private final PlayerChangeListener playerChangeListener = (childPos, player) -> {
// todo
pagerPlayer = player;
};
public FeedAdapter(final Activity activity, final ArrayList<FeedModel> FeedModels, final MentionClickListener mentionClickListener) {
this.activity = activity;
this.feedModels = FeedModels;
this.mentionClickListener = mentionClickListener;
this.layoutInflater = LayoutInflater.from(activity);
}
@NonNull
@Override
public FeedItemViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final View view;
if (viewType == MediaItemType.MEDIA_TYPE_VIDEO.ordinal())
view = layoutInflater.inflate(R.layout.item_feed_video, parent, false);
else if (viewType == MediaItemType.MEDIA_TYPE_SLIDER.ordinal())
view = layoutInflater.inflate(R.layout.item_feed_slider, parent, false);
else
view = layoutInflater.inflate(R.layout.item_feed, parent, false);
return new FeedItemViewHolder(view);
}
@SuppressLint("SetTextI18n")
@Override
public void onBindViewHolder(@NonNull final FeedItemViewHolder viewHolder, final int position) {
final FeedModel feedModel = feedModels.get(position);
if (feedModel != null) {
final RequestManager glideRequestManager = Glide.with(viewHolder.itemView);
feedModel.setPosition(position);
viewHolder.viewPost.setTag(feedModel);
viewHolder.profilePic.setTag(feedModel);
viewHolder.btnDownload.setTag(feedModel);
viewHolder.viewerCaption.setTag(feedModel);
final ProfileModel profileModel = feedModel.getProfileModel();
if (profileModel != null) {
glideRequestManager.load(profileModel.getSdProfilePic()).into(viewHolder.profilePic);
viewHolder.username.setText(profileModel.getUsername());
}
viewHolder.viewPost.setOnClickListener(clickListener);
viewHolder.profilePic.setOnClickListener(clickListener);
viewHolder.btnDownload.setOnClickListener(clickListener);
viewHolder.tvPostDate.setText(feedModel.getPostDate());
final long commentsCount = feedModel.getCommentsCount();
viewHolder.commentsCount.setText(String.valueOf(commentsCount));
if (commentsCount <= 0) {
viewHolder.btnComments.setTag(null);
viewHolder.btnComments.setOnClickListener(null);
viewHolder.btnComments.setEnabled(false);
} else {
viewHolder.btnComments.setTag(feedModel);
viewHolder.btnComments.setOnClickListener(clickListener);
viewHolder.btnComments.setEnabled(true);
}
final String thumbnailUrl = feedModel.getThumbnailUrl();
final String displayUrl = feedModel.getDisplayUrl();
CharSequence postCaption = feedModel.getPostCaption();
final boolean captionEmpty = Utils.isEmpty(postCaption);
viewHolder.viewerCaption.setOnClickListener(clickListener);
viewHolder.viewerCaption.setOnLongClickListener(longClickListener);
viewHolder.viewerCaption.setVisibility(captionEmpty ? View.GONE : View.VISIBLE);
if (!captionEmpty && Utils.hasMentions(postCaption)) {
postCaption = Utils.getMentionText(postCaption);
feedModel.setPostCaption(postCaption);
viewHolder.viewerCaption.setText(postCaption, TextView.BufferType.SPANNABLE);
viewHolder.viewerCaption.setMentionClickListener(mentionClickListener);
} else {
viewHolder.viewerCaption.setText(postCaption);
}
expandCollapseTextView(viewHolder.viewerCaption, feedModel);
final MediaItemType itemType = feedModel.getItemType();
final View viewToChangeHeight;
if (itemType == MediaItemType.MEDIA_TYPE_VIDEO) {
viewToChangeHeight = viewHolder.playerView;
viewHolder.videoViewsParent.setVisibility(View.VISIBLE);
viewHolder.videoViews.setText(String.valueOf(feedModel.getViewCount()));
} else {
viewHolder.videoViewsParent.setVisibility(View.GONE);
viewHolder.btnMute.setVisibility(View.GONE);
if (itemType == MediaItemType.MEDIA_TYPE_SLIDER) {
viewToChangeHeight = viewHolder.mediaList;
final ViewerPostModel[] sliderItems = feedModel.getSliderItems();
final int sliderItemLen = sliderItems != null ? sliderItems.length : 0;
if (sliderItemLen > 0) {
viewHolder.mediaCounter.setText("1/" + sliderItemLen);
viewHolder.mediaList.setOffscreenPageLimit(Math.min(5, sliderItemLen));
final ViewPager.SimpleOnPageChangeListener simpleOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
private int prevPos = 0;
@Override
public void onPageSelected(final int position) {
ViewerPostModel sliderItem = sliderItems[prevPos];
if (sliderItem != null) sliderItem.setSelected(false);
sliderItem = sliderItems[position];
if (sliderItem != null) sliderItem.setSelected(true);
View childAt = viewHolder.mediaList.getChildAt(prevPos);
if (childAt instanceof PlayerView) {
pagerPlayer = (SimpleExoPlayer) ((PlayerView) childAt).getPlayer();
if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(false);
}
childAt = viewHolder.mediaList.getChildAt(position);
if (childAt instanceof PlayerView) {
pagerPlayer = (SimpleExoPlayer) ((PlayerView) childAt).getPlayer();
if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(true);
}
prevPos = position;
viewHolder.mediaCounter.setText((position + 1) + "/" + sliderItemLen);
}
};
//noinspection deprecation
viewHolder.mediaList.setOnPageChangeListener(simpleOnPageChangeListener); // cause add listeners might add to recycled holders
final View.OnClickListener muteClickListener = v -> {
Player player = null;
if (v instanceof PlayerView) player = ((PlayerView) v).getPlayer();
else if (v instanceof ImageView || v == viewHolder.btnMute) {
final int currentItem = viewHolder.mediaList.getCurrentItem();
if (currentItem < viewHolder.mediaList.getChildCount()) {
final View childAt = viewHolder.mediaList.getChildAt(currentItem);
if (childAt instanceof PlayerView) player = ((PlayerView) childAt).getPlayer();
}
} else {
final Object tag = v.getTag();
if (tag instanceof Player) player = (Player) tag;
}
if (player instanceof SimpleExoPlayer) {
final SimpleExoPlayer exoPlayer = (SimpleExoPlayer) player;
final float intVol = exoPlayer.getVolume() == 0f ? 1f : 0f;
exoPlayer.setVolume(intVol);
viewHolder.btnMute.setImageResource(intVol == 0f ? R.drawable.vol : R.drawable.mute);
Utils.sessionVolumeFull = intVol == 1f;
}
};
viewHolder.btnMute.setOnClickListener(muteClickListener);
viewHolder.mediaList.setAdapter(new ChildMediaItemsAdapter(sliderItems, viewHolder.btnMute, muteClickListener, playerChangeListener));
}
} else {
viewToChangeHeight = viewHolder.imageView;
String url = displayUrl;
if (Utils.isEmpty(url)) url = thumbnailUrl;
glideRequestManager.load(url).into(viewHolder.imageView);
}
}
if (viewToChangeHeight != null) {
final ViewGroup.LayoutParams layoutParams = viewToChangeHeight.getLayoutParams();
layoutParams.height = Utils.displayMetrics.widthPixels + 1;
viewToChangeHeight.setLayoutParams(layoutParams);
}
}
}
@Override
public int getItemCount() {
return feedModels == null ? 0 : feedModels.size();
}
@Override
public int getItemViewType(final int position) {
if (feedModels != null) return feedModels.get(position).getItemType().ordinal();
return MediaItemType.MEDIA_TYPE_IMAGE.ordinal();
}
/**
* expands or collapses {@link RamboTextView} [stg idek why i wrote this documentation]
*
* @param textView the {@link RamboTextView} view, to expand and collapse
* @param feedModel the {@link FeedModel} model to check wether model is collapsed to expanded
*
* @return true if expanded/collapsed, false if empty or text size is <= 255 chars
*/
public static boolean expandCollapseTextView(@NonNull final RamboTextView textView, @NonNull final FeedModel feedModel) {
final CharSequence caption = feedModel.getPostCaption();
if (Utils.isEmpty(caption)) return false;
final TextView.BufferType bufferType = caption instanceof Spanned ? TextView.BufferType.SPANNABLE : TextView.BufferType.NORMAL;
if (!feedModel.isCaptionExpanded()) {
int i = Utils.indexOfChar(caption, '\r', 0);
if (i == -1) i = Utils.indexOfChar(caption, '\n', 0);
if (i == -1) i = 255;
final int captionLen = caption.length();
final int minTrim = Math.min(255, i);
if (captionLen <= minTrim) return false;
final CharSequence mentionText = caption.subSequence(0, Math.min(captionLen, minTrim));
final SpannableStringBuilder stringBuilder = new SpannableStringBuilder(mentionText).append(ellipsize);
final int spanLen = stringBuilder.length();
// fixed @mention...more merging into one span
final CommentMentionClickSpan[] spans = stringBuilder.getSpans(0, mentionText.length(), CommentMentionClickSpan.class);
if (spans != null) {
for (final CommentMentionClickSpan span : spans) {
final int spanStart = stringBuilder.getSpanStart(span);
stringBuilder.removeSpan(span);
stringBuilder.setSpan(span, spanStart, mentionText.length(), 0);
}
}
stringBuilder.setSpan(new StyleSpan(Typeface.BOLD), spanLen - ellipsize.length(), spanLen, 0);
textView.setText(stringBuilder, bufferType);
textView.setCaptionIsExpandable(true);
textView.setCaptionIsExpanded(true);
} else {
textView.setText(caption, bufferType);
textView.setCaptionIsExpanded(false);
}
return true;
}
private interface PlayerChangeListener {
void playerChanged(final int childPos, final SimpleExoPlayer player);
}
private static final class ChildMediaItemsAdapter extends PagerAdapter {
private final PlayerChangeListener playerChangeListener;
private final View.OnClickListener muteClickListener;
private final ViewerPostModel[] sliderItems;
private final View btnMute;
private SimpleExoPlayer player;
private ChildMediaItemsAdapter(final ViewerPostModel[] sliderItems, final View btnMute, final View.OnClickListener muteClickListener,
final PlayerChangeListener playerChangeListener) {
this.muteClickListener = muteClickListener;
this.sliderItems = sliderItems;
this.btnMute = btnMute;
if (BuildConfig.DEBUG) this.playerChangeListener = playerChangeListener;
else this.playerChangeListener = null;
}
@NonNull
@Override
public Object instantiateItem(@NonNull final ViewGroup container, final int position) {
if (BuildConfig.DEBUG) container.setBackgroundColor(0xFF_0a_c0_09); // todo remove
final Context context = container.getContext();
final ViewerPostModel sliderItem = sliderItems[position];
if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) {
if (btnMute != null) btnMute.setVisibility(View.VISIBLE);
final PlayerView playerView = new PlayerView(context);
player = new SimpleExoPlayer.Builder(context).build();
playerView.setPlayer(player);
float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f;
if (vol == 0f && Utils.sessionVolumeFull) vol = 1f;
player.setVolume(vol);
player.setPlayWhenReady(Utils.settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS));
final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(new DefaultDataSourceFactory(context, "instagram"))
.createMediaSource(Uri.parse(sliderItem.getDisplayUrl()));
player.setRepeatMode(Player.REPEAT_MODE_ALL);
player.prepare(mediaSource);
player.setVolume(vol);
playerView.setTag(player);
playerView.setOnClickListener(muteClickListener);
if (playerChangeListener != null) {
//todo
// playerChangeListener.playerChanged(position, player);
Log.d("AWAISKING_APP", "playerChangeListener: " + playerChangeListener);
}
container.addView(playerView);
return playerView;
} else {
if (btnMute != null) btnMute.setVisibility(View.GONE);
final PhotoView photoView = new PhotoView(context);
photoView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
Glide.with(context).load(sliderItem.getDisplayUrl()).into(photoView);
container.addView(photoView);
return photoView;
}
}
@Override
public void destroyItem(@NonNull final ViewGroup container, final int position, @NonNull final Object object) {
final Player player = object instanceof PlayerView ? ((PlayerView) object).getPlayer() : this.player;
if (player == this.player && this.player != null) {
this.player.stop(true);
this.player.release();
} else if (player != null) {
player.stop(true);
player.release();
}
container.removeView((View) object);
}
@Override
public int getCount() {
return sliderItems != null ? sliderItems.length : 0;
}
@Override
public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) {
return view == object;
}
}
}

57
app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java

@ -0,0 +1,57 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.HighlightViewHolder;
import awais.instagrabber.models.FeedStoryModel;
import awais.instagrabber.models.ProfileModel;
public final class FeedStoriesAdapter extends RecyclerView.Adapter<HighlightViewHolder> {
private final View.OnClickListener clickListener;
private LayoutInflater layoutInflater;
private FeedStoryModel[] feedStoryModels;
public FeedStoriesAdapter(final FeedStoryModel[] feedStoryModels, final View.OnClickListener clickListener) {
this.feedStoryModels = feedStoryModels;
this.clickListener = clickListener;
}
@NonNull
@Override
public HighlightViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
if (layoutInflater == null) layoutInflater = LayoutInflater.from(parent.getContext());
return new HighlightViewHolder(layoutInflater.inflate(R.layout.item_highlight, parent, false));
}
@Override
public void onBindViewHolder(@NonNull final HighlightViewHolder holder, final int position) {
final FeedStoryModel feedStoryModel = feedStoryModels[position];
if (feedStoryModel != null) {
holder.itemView.setTag(feedStoryModel);
holder.itemView.setOnClickListener(clickListener);
final ProfileModel profileModel = feedStoryModel.getProfileModel();
holder.title.setText(profileModel.getUsername());
Glide.with(layoutInflater.getContext()).load(profileModel.getSdProfilePic()).into(holder.icon);
}
}
public void setData(final FeedStoryModel[] feedStoryModels) {
this.feedStoryModels = feedStoryModels;
notifyDataSetChanged();
}
@Override
public int getItemCount() {
return feedStoryModels == null ? 0 : feedStoryModels.length;
}
}

144
app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java

@ -0,0 +1,144 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.FollowsViewHolder;
import awais.instagrabber.interfaces.OnGroupClickListener;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.utils.Utils;
import thoughtbot.expandableadapter.ExpandableGroup;
import thoughtbot.expandableadapter.ExpandableList;
import thoughtbot.expandableadapter.ExpandableListPosition;
import thoughtbot.expandableadapter.GroupViewHolder;
// thanks to ThoughtBot's ExpandableRecyclerViewAdapter
// https://github.com/thoughtbot/expandable-recycler-view
public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements OnGroupClickListener, Filterable {
private final Filter filter = new Filter() {
@Nullable
@Override
protected FilterResults performFiltering(final CharSequence filter) {
if (expandableList.groups != null) {
final boolean isFilterEmpty = Utils.isEmpty(filter);
final String query = isFilterEmpty ? null : filter.toString().toLowerCase();
for (int x = 0; x < expandableList.groups.size(); ++x) {
final ExpandableGroup expandableGroup = expandableList.groups.get(x);
final List<FollowModel> items = expandableGroup.getItems(false);
final int itemCount = expandableGroup.getItemCount(false);
for (int i = 0; i < itemCount; ++i) {
final FollowModel followModel = items.get(i);
if (isFilterEmpty) followModel.setShown(true);
else followModel.setShown(Utils.hasKey(query, followModel.getUsername(), followModel.getFullName()));
}
}
}
return null;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
notifyDataSetChanged();
}
};
private final View.OnClickListener onClickListener;
private final LayoutInflater layoutInflater;
private final ExpandableList expandableList;
private final boolean hasManyGroups;
public FollowAdapter(final Context context, final View.OnClickListener onClickListener, @NonNull final ArrayList<ExpandableGroup> groups) {
this.layoutInflater = LayoutInflater.from(context);
this.expandableList = new ExpandableList(groups);
this.onClickListener = onClickListener;
this.hasManyGroups = groups.size() > 1;
}
@Override
public Filter getFilter() {
return filter;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final boolean isGroup = hasManyGroups && viewType == ExpandableListPosition.GROUP;
final View view = layoutInflater.inflate(isGroup ? R.layout.header_follow : R.layout.item_follow, parent, false);
return isGroup ? new GroupViewHolder(view, this) : new FollowsViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
final ExpandableListPosition listPos = expandableList.getUnflattenedPosition(position);
final ExpandableGroup group = expandableList.getExpandableGroup(listPos);
if (hasManyGroups && listPos.type == ExpandableListPosition.GROUP) {
final GroupViewHolder gvh = (GroupViewHolder) holder;
gvh.setTitle(group.getTitle());
gvh.toggle(isGroupExpanded(group));
} else {
final FollowModel model = group.getItems(true).get(hasManyGroups ? listPos.childPos : position);
final FollowsViewHolder followHolder = (FollowsViewHolder) holder;
if (model != null) {
followHolder.itemView.setTag(model);
followHolder.itemView.setOnClickListener(onClickListener);
followHolder.tvUsername.setText(model.getUsername());
followHolder.tvFullName.setText(model.getFullName());
Glide.with(layoutInflater.getContext()).load(model.getProfilePicUrl()).into(followHolder.profileImage);
}
}
}
@Override
public int getItemCount() {
return expandableList.getVisibleItemCount() - (hasManyGroups ? 0 : 1);
}
@Override
public int getItemViewType(final int position) {
return !hasManyGroups ? 0 : expandableList.getUnflattenedPosition(position).type;
}
@Override
public void toggleGroup(final int flatPos) {
final ExpandableListPosition listPosition = expandableList.getUnflattenedPosition(flatPos);
final int groupPos = listPosition.groupPos;
final int positionStart = expandableList.getFlattenedGroupIndex(listPosition) + 1;
final int positionEnd = expandableList.groups.get(groupPos).getItemCount(true);
final boolean isExpanded = expandableList.expandedGroupIndexes[groupPos];
expandableList.expandedGroupIndexes[groupPos] = !isExpanded;
notifyItemChanged(positionStart - 1);
if (positionEnd > 0) {
if (isExpanded) notifyItemRangeRemoved(positionStart, positionEnd);
else notifyItemRangeInserted(positionStart, positionEnd);
}
}
public boolean isGroupExpanded(final ExpandableGroup group) {
return expandableList.expandedGroupIndexes[expandableList.groups.indexOf(group)];
}
}

53
app/src/main/java/awais/instagrabber/adapters/HighlightsAdapter.java

@ -0,0 +1,53 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.HighlightViewHolder;
import awais.instagrabber.models.HighlightModel;
public final class HighlightsAdapter extends RecyclerView.Adapter<HighlightViewHolder> {
private final View.OnClickListener clickListener;
private LayoutInflater layoutInflater;
private HighlightModel[] highlightModels;
public HighlightsAdapter(final HighlightModel[] highlightModels, final View.OnClickListener clickListener) {
this.highlightModels = highlightModels;
this.clickListener = clickListener;
}
@NonNull
@Override
public HighlightViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
if (layoutInflater == null) layoutInflater = LayoutInflater.from(parent.getContext());
return new HighlightViewHolder(layoutInflater.inflate(R.layout.item_highlight, parent, false));
}
@Override
public void onBindViewHolder(@NonNull final HighlightViewHolder holder, final int position) {
final HighlightModel highlightModel = highlightModels[position];
if (highlightModel != null) {
holder.itemView.setTag(highlightModel);
holder.itemView.setOnClickListener(clickListener);
holder.title.setText(highlightModel.getTitle());
Glide.with(holder.itemView).load(highlightModel.getThumbnailUrl()).into(holder.icon);
}
}
public void setData(final HighlightModel[] highlightModels) {
this.highlightModels = highlightModels;
notifyDataSetChanged();
}
@Override
public int getItemCount() {
return highlightModels == null ? 0 : highlightModels.length;
}
}

354
app/src/main/java/awais/instagrabber/adapters/MessageItemsAdapter.java

@ -0,0 +1,354 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.text.Spanned;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import java.util.ArrayList;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.directmessages.TextMessageViewHolder;
import awais.instagrabber.interfaces.MentionClickListener;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.models.direct_messages.DirectItemModel;
import awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemMediaModel;
import awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemRavenMediaModel;
import awais.instagrabber.models.enums.DirectItemType;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.models.enums.RavenExpiringMediaType;
import awais.instagrabber.models.enums.RavenMediaViewType;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemLinkContext;
import static awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemLinkModel;
import static awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemReelShareModel;
import static awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemVoiceMediaModel;
import static awais.instagrabber.models.direct_messages.DirectItemModel.RavenExpiringMediaActionSummaryModel;
public final class MessageItemsAdapter extends RecyclerView.Adapter<TextMessageViewHolder> {
private static final int MESSAGE_INCOMING = 69, MESSAGE_OUTGOING = 420;
private final ProfileModel myProfileHolder = new ProfileModel(false, false, null, null, null, null, null, null, null, 0, 0, 0);
private final ArrayList<DirectItemModel> directItemModels;
private final ArrayList<ProfileModel> users;
private final View.OnClickListener onClickListener;
private final MentionClickListener mentionClickListener;
private final View.OnClickListener openProfileClickListener = v -> {
final Object tag = v.getTag();
if (tag instanceof ProfileModel) {
// todo do profile stuff
final ProfileModel profileModel = (ProfileModel) tag;
Log.d("AWAISKING_APP", "--> " + profileModel);
}
};
private final int itemMargin;
private DirectItemVoiceMediaModel prevVoiceModel;
private ImageView prevPlayIcon;
private final View.OnClickListener voicePlayClickListener = v -> {
final Object tag = v.getTag();
if (v instanceof ViewGroup && tag instanceof DirectItemVoiceMediaModel) {
final ImageView playIcon = (ImageView) ((ViewGroup) v).getChildAt(0);
final DirectItemVoiceMediaModel voiceMediaModel = (DirectItemVoiceMediaModel) tag;
final boolean voicePlaying = voiceMediaModel.isPlaying();
voiceMediaModel.setPlaying(!voicePlaying);
if (voiceMediaModel == prevVoiceModel) {
// todo pause / resume
} else {
// todo release prev audio, start new voice
if (prevVoiceModel != null) prevVoiceModel.setPlaying(false);
if (prevPlayIcon != null) prevPlayIcon.setImageResource(android.R.drawable.ic_media_play);
}
if (voicePlaying) {
playIcon.setImageResource(android.R.drawable.ic_media_play);
} else {
playIcon.setImageResource(android.R.drawable.ic_media_pause);
}
prevVoiceModel = voiceMediaModel;
prevPlayIcon = playIcon;
}
};
private Context context;
private LayoutInflater layoutInflater;
private String strDmYou;
public MessageItemsAdapter(final ArrayList<DirectItemModel> directItemModels, final ArrayList<ProfileModel> users,
final View.OnClickListener onClickListener, final MentionClickListener mentionClickListener) {
this.users = users;
this.directItemModels = directItemModels;
this.onClickListener = onClickListener;
this.mentionClickListener = mentionClickListener;
this.itemMargin = Utils.displayMetrics.widthPixels / 5;
}
@NonNull
@Override
public TextMessageViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
if (context == null) context = parent.getContext();
if (strDmYou == null) strDmYou = context.getString(R.string.direct_messages_you);
if (layoutInflater == null) layoutInflater = LayoutInflater.from(context);
return new TextMessageViewHolder(layoutInflater.inflate(R.layout.item_message_item, parent, false),
onClickListener, mentionClickListener);
}
@Override
public void onBindViewHolder(@NonNull final TextMessageViewHolder holder, final int position) {
final DirectItemModel directItemModel = directItemModels.get(position);
holder.itemView.setTag(directItemModel);
if (directItemModel != null) {
final DirectItemType itemType = directItemModel.getItemType();
final ProfileModel user = getUser(directItemModel.getUserId());
final int type = user == myProfileHolder ? MESSAGE_OUTGOING : MESSAGE_INCOMING;
final RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) holder.itemView.getLayoutParams();
layoutParams.setMargins(type == MESSAGE_OUTGOING ? itemMargin : 0, 0,
type == MESSAGE_INCOMING ? itemMargin : 0, 0);
holder.tvMessage.setVisibility(View.GONE);
holder.voiceMessageContainer.setVisibility(View.GONE);
holder.ivAnimatedMessage.setVisibility(View.GONE);
holder.linkMessageContainer.setVisibility(View.GONE);
holder.mediaMessageContainer.setVisibility(View.GONE);
holder.mediaTypeIcon.setVisibility(View.GONE);
holder.mediaExpiredIcon.setVisibility(View.GONE);
holder.profileMessageContainer.setVisibility(View.GONE);
holder.isVerified.setVisibility(View.GONE);
holder.btnOpenProfile.setVisibility(View.GONE);
holder.btnOpenProfile.setOnClickListener(null);
holder.btnOpenProfile.setTag(null);
CharSequence text = "?";
if (user != null && user != myProfileHolder) text = user.getUsername();
else if (user == myProfileHolder) text = strDmYou;
text = text + " - " + directItemModel.getDateTime();
holder.tvUsername.setText(text);
holder.ivProfilePic.setVisibility(type == MESSAGE_INCOMING ? View.VISIBLE : View.GONE);
final RequestManager glideRequestManager = Glide.with(holder.itemView);
if (type == MESSAGE_INCOMING && user != null)
glideRequestManager.load(user.getSdProfilePic()).into(holder.ivProfilePic);
DirectItemMediaModel mediaModel = directItemModel.getMediaModel();
switch (itemType) {
case PLACEHOLDER:
case TEXT:
text = directItemModel.getText();
text = Utils.getSpannableUrl(text.toString()); // for urls
if (Utils.hasMentions(text)) text = Utils.getMentionText(text); // for mentions
if (text instanceof Spanned) holder.tvMessage.setText(text, TextView.BufferType.SPANNABLE);
else if (text == "") holder.tvMessage.setText(context.getText(R.string.dms_inbox_raven_message_unknown));
else holder.tvMessage.setText(text);
holder.tvMessage.setVisibility(View.VISIBLE);
break;
case LINK: {
final DirectItemLinkModel link = directItemModel.getLinkModel();
final DirectItemLinkContext linkContext = link.getLinkContext();
final String linkImageUrl = linkContext.getLinkImageUrl();
if (!Utils.isEmpty(linkImageUrl)) {
glideRequestManager.load(linkImageUrl).into(holder.ivLinkPreview);
holder.tvLinkTitle.setText(linkContext.getLinkTitle());
holder.tvLinkSummary.setText(linkContext.getLinkSummary());
holder.ivLinkPreview.setVisibility(View.VISIBLE);
holder.linkMessageContainer.setVisibility(View.VISIBLE);
}
holder.tvMessage.setText(Utils.getSpannableUrl(link.getText()));
holder.tvMessage.setVisibility(View.VISIBLE);
}
break;
case MEDIA_SHARE: {
final ProfileModel modelUser = mediaModel.getUser();
if (modelUser != null) {
holder.tvMessage.setText(context.getString(R.string.dms_inbox_media_shared_from, modelUser.getUsername()));
holder.tvMessage.setVisibility(View.VISIBLE);
}
}
case MEDIA: {
glideRequestManager.load(mediaModel.getThumbUrl()).into(holder.ivMediaPreview);
final MediaItemType modelMediaType = mediaModel.getMediaType();
holder.mediaTypeIcon.setVisibility(modelMediaType == MediaItemType.MEDIA_TYPE_VIDEO ||
modelMediaType == MediaItemType.MEDIA_TYPE_SLIDER ? View.VISIBLE : View.GONE);
holder.mediaMessageContainer.setVisibility(View.VISIBLE);
}
break;
case RAVEN_MEDIA: {
final DirectItemRavenMediaModel ravenMediaModel = directItemModel.getRavenMediaModel();
final RavenExpiringMediaActionSummaryModel mediaActionSummary = ravenMediaModel.getExpiringMediaActionSummary();
mediaModel = ravenMediaModel.getMedia();
final boolean isExpired = mediaModel == null ||
Utils.isEmpty(mediaModel.getThumbUrl()) && mediaModel.getPk() < 1;
holder.mediaExpiredIcon.setVisibility(isExpired ? View.VISIBLE : View.GONE);
int textRes = R.string.dms_inbox_raven_media_unknown;
if (isExpired) textRes = R.string.dms_inbox_raven_media_expired;
if (!isExpired && mediaActionSummary != null) {
final RavenExpiringMediaType expiringMediaType = mediaActionSummary.getType();
if (expiringMediaType == RavenExpiringMediaType.RAVEN_DELIVERED)
textRes = R.string.dms_inbox_raven_media_delivered;
else if (expiringMediaType == RavenExpiringMediaType.RAVEN_SENT)
textRes = R.string.dms_inbox_raven_media_sent;
else if (expiringMediaType == RavenExpiringMediaType.RAVEN_OPENED)
textRes = R.string.dms_inbox_raven_media_opened;
else if (expiringMediaType == RavenExpiringMediaType.RAVEN_REPLAYED)
textRes = R.string.dms_inbox_raven_media_replayed;
else if (expiringMediaType == RavenExpiringMediaType.RAVEN_SENDING)
textRes = R.string.dms_inbox_raven_media_sending;
else if (expiringMediaType == RavenExpiringMediaType.RAVEN_BLOCKED)
textRes = R.string.dms_inbox_raven_media_blocked;
else if (expiringMediaType == RavenExpiringMediaType.RAVEN_SUGGESTED)
textRes = R.string.dms_inbox_raven_media_suggested;
else if (expiringMediaType == RavenExpiringMediaType.RAVEN_SCREENSHOT)
textRes = R.string.dms_inbox_raven_media_screenshot;
else if (expiringMediaType == RavenExpiringMediaType.RAVEN_CANNOT_DELIVER)
textRes = R.string.dms_inbox_raven_media_cant_deliver;
final RavenMediaViewType ravenMediaViewType = ravenMediaModel.getViewType();
if (ravenMediaViewType == RavenMediaViewType.PERMANENT || ravenMediaViewType == RavenMediaViewType.REPLAYABLE) {
final MediaItemType mediaType = mediaModel.getMediaType();
holder.mediaTypeIcon.setVisibility(mediaType == MediaItemType.MEDIA_TYPE_VIDEO ||
mediaType == MediaItemType.MEDIA_TYPE_SLIDER ? View.VISIBLE : View.GONE);
glideRequestManager.load(mediaModel.getThumbUrl()).into(holder.ivMediaPreview);
holder.mediaMessageContainer.setVisibility(View.VISIBLE);
}
}
holder.tvMessage.setText(context.getText(textRes));
holder.tvMessage.setVisibility(View.VISIBLE);
}
break;
case REEL_SHARE: {
final DirectItemReelShareModel reelShare = directItemModel.getReelShare();
if (!Utils.isEmpty(text = reelShare.getText())) {
holder.tvMessage.setText(text);
holder.tvMessage.setVisibility(View.VISIBLE);
}
final DirectItemMediaModel reelShareMedia = reelShare.getMedia();
final MediaItemType mediaType = reelShareMedia.getMediaType();
Log.d("austin_debug", "media: " + reelShareMedia);
holder.mediaTypeIcon.setVisibility(mediaType == MediaItemType.MEDIA_TYPE_VIDEO ||
mediaType == MediaItemType.MEDIA_TYPE_SLIDER ? View.VISIBLE : View.GONE);
glideRequestManager.load(reelShareMedia.getThumbUrl()).into(holder.ivMediaPreview);
holder.mediaMessageContainer.setVisibility(View.VISIBLE);
}
break;
case VOICE_MEDIA: {
final DirectItemVoiceMediaModel voiceMediaModel = directItemModel.getVoiceMediaModel();
if (voiceMediaModel != null) {
final int[] waveformData = voiceMediaModel.getWaveformData();
if (waveformData != null) holder.waveformSeekBar.setSample(waveformData);
final long durationMs = voiceMediaModel.getDurationMs();
holder.tvVoiceDuration.setText(Utils.millisToString(durationMs));
holder.waveformSeekBar.setProgress(voiceMediaModel.getProgress());
holder.waveformSeekBar.setProgressChangeListener((waveformSeekBar, progress, fromUser) -> {
// todo progress audio player
voiceMediaModel.setProgress(progress);
if (fromUser)
holder.tvVoiceDuration.setText(Utils.millisToString(durationMs * progress / 100));
});
holder.btnPlayVoice.setTag(voiceMediaModel);
holder.btnPlayVoice.setOnClickListener(voicePlayClickListener);
} else {
holder.waveformSeekBar.setProgress(0);
}
holder.voiceMessageContainer.setVisibility(View.VISIBLE);
}
break;
case ANIMATED_MEDIA: {
glideRequestManager.asGif().load(directItemModel.getAnimatedMediaModel().getGifUrl())
.into(holder.ivAnimatedMessage);
holder.ivAnimatedMessage.setVisibility(View.VISIBLE);
}
break;
case PROFILE: {
final ProfileModel profileModel = directItemModel.getProfileModel();
Glide.with(holder.ivMessageProfilePic).load(profileModel.getSdProfilePic())
.into(holder.ivMessageProfilePic);
holder.btnOpenProfile.setTag(profileModel);
holder.btnOpenProfile.setOnClickListener(openProfileClickListener);
holder.tvProfileName.setText(profileModel.getName());
holder.tvProfileUsername.setText(profileModel.getUsername());
holder.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE);
holder.btnOpenProfile.setVisibility(View.VISIBLE);
holder.profileMessageContainer.setVisibility(View.VISIBLE);
}
break;
case VIDEO_CALL_EVENT: {
// todo add call event info
holder.tvMessage.setVisibility(View.VISIBLE);
holder.itemView.setBackgroundColor(0xFF_1F90E6); // blue bitch
}
break;
}
}
}
@Override
public int getItemViewType(final int position) {
return directItemModels.get(position).getItemType().ordinal();
}
@Override
public int getItemCount() {
return directItemModels == null ? 0 : directItemModels.size();
}
@Nullable
private ProfileModel getUser(final long userId) {
if (users != null) {
for (final ProfileModel user : users)
if (Long.toString(userId).equals(user.getId())) return user;
return myProfileHolder;
}
return null;
}
}

92
app/src/main/java/awais/instagrabber/adapters/PostsAdapter.java

@ -0,0 +1,92 @@
package awais.instagrabber.adapters;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import java.util.ArrayList;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.PostViewHolder;
import awais.instagrabber.models.PostModel;
import awais.instagrabber.models.enums.MediaItemType;
public final class PostsAdapter extends RecyclerView.Adapter<PostViewHolder> {
private final ArrayList<PostModel> postModels;
private final View.OnClickListener clickListener;
private final View.OnLongClickListener longClickListener;
private LayoutInflater layoutInflater;
public boolean isSelecting = false;
public PostsAdapter(final ArrayList<PostModel> postModels, final View.OnClickListener clickListener,
final View.OnLongClickListener longClickListener) {
this.postModels = postModels;
this.clickListener = clickListener;
this.longClickListener = longClickListener;
}
@NonNull
@Override
public PostViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
if (layoutInflater == null) layoutInflater = LayoutInflater.from(parent.getContext());
return new PostViewHolder(layoutInflater.inflate(R.layout.item_post, parent, false));
}
@Override
public void onBindViewHolder(@NonNull final PostViewHolder holder, final int position) {
final PostModel postModel = postModels.get(position);
if (postModel != null) {
postModel.setPosition(position);
holder.itemView.setTag(postModel);
holder.itemView.setOnClickListener(clickListener);
holder.itemView.setOnLongClickListener(longClickListener);
final MediaItemType itemType = postModel.getItemType();
final boolean isSlider = itemType == MediaItemType.MEDIA_TYPE_SLIDER;
holder.isDownloaded.setVisibility(postModel.isDownloaded() ? View.VISIBLE : View.GONE);
holder.typeIcon.setVisibility(itemType == MediaItemType.MEDIA_TYPE_VIDEO || isSlider ? View.VISIBLE : View.GONE);
holder.typeIcon.setImageResource(isSlider ? R.drawable.slider : R.drawable.video);
holder.selectedView.setVisibility(postModel.isSelected() ? View.VISIBLE : View.GONE);
holder.progressView.setVisibility(View.VISIBLE);
final RequestManager glideRequestManager = Glide.with(holder.postImage);
glideRequestManager.load(postModel.getThumbnailUrl()).listener(new RequestListener<Drawable>() {
@Override
public boolean onResourceReady(final Drawable resource, final Object model, final Target<Drawable> target, final DataSource dataSource, final boolean isFirstResource) {
holder.progressView.setVisibility(View.GONE);
return false;
}
@Override
public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target<Drawable> target, final boolean isFirstResource) {
holder.progressView.setVisibility(View.GONE);
glideRequestManager.load(postModel.getDisplayUrl()).into(holder.postImage);
return false;
}
}).into(holder.postImage);
}
}
@Override
public int getItemCount() {
return postModels == null ? 0 : postModels.size();
}
}

68
app/src/main/java/awais/instagrabber/adapters/PostsMediaAdapter.java

@ -0,0 +1,68 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.PostMediaViewHolder;
import awais.instagrabber.models.BasePostModel;
import awais.instagrabber.models.ViewerPostModel;
public final class PostsMediaAdapter extends RecyclerView.Adapter<PostMediaViewHolder> {
private final View.OnClickListener clickListener;
private LayoutInflater layoutInflater;
private ViewerPostModel[] postModels;
public PostsMediaAdapter(final ViewerPostModel[] postModels, final View.OnClickListener clickListener) {
this.postModels = postModels;
this.clickListener = clickListener;
}
@NonNull
@Override
public PostMediaViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
if (layoutInflater == null) layoutInflater = LayoutInflater.from(parent.getContext());
return new PostMediaViewHolder(layoutInflater.inflate(R.layout.item_child_post, parent, false));
}
@Override
public void onBindViewHolder(@NonNull final PostMediaViewHolder holder, final int position) {
final ViewerPostModel postModel = postModels[position];
if (postModel != null) {
postModel.setPosition(position);
holder.itemView.setTag(postModel);
holder.itemView.setOnClickListener(clickListener);
holder.selectedView.setVisibility(postModel.isCurrentSlide() ? View.VISIBLE : View.GONE);
holder.isDownloaded.setVisibility(postModel.isDownloaded() ? View.VISIBLE : View.GONE);
Glide.with(layoutInflater.getContext()).load(postModel.getSliderDisplayUrl()).into(holder.icon);
}
}
public void setData(final ViewerPostModel[] postModels) {
this.postModels = postModels;
notifyDataSetChanged();
}
public ViewerPostModel getItemAt(final int position) {
return postModels == null ? null : postModels[position];
}
@Override
public int getItemCount() {
return postModels == null ? 0 : postModels.length;
}
public BasePostModel[] getPostModels() {
return postModels;
}
}

75
app/src/main/java/awais/instagrabber/adapters/SimpleAdapter.java

@ -0,0 +1,75 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.utils.DataBox;
public final class SimpleAdapter<T> extends RecyclerView.Adapter<SimpleAdapter.SimpleViewHolder> {
private List<T> items;
private final LayoutInflater layoutInflater;
private final View.OnClickListener onClickListener;
private final View.OnLongClickListener longClickListener;
public SimpleAdapter(final Context context, final List<T> items, final View.OnClickListener onClickListener) {
this(context, items, onClickListener, null);
}
public SimpleAdapter(final Context context, final List<T> items, final View.OnClickListener onClickListener,
final View.OnLongClickListener longClickListener) {
this.layoutInflater = LayoutInflater.from(context);
this.items = items;
this.onClickListener = onClickListener;
this.longClickListener = longClickListener;
}
public void setItems(final List<T> items) {
this.items = items;
notifyDataSetChanged();
}
@NonNull
@Override
public SimpleViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
return new SimpleViewHolder(layoutInflater.
inflate(R.layout.item_dir_list, parent, false), onClickListener, longClickListener);
}
@Override
public void onBindViewHolder(@NonNull final SimpleViewHolder holder, final int position) {
final T item = items.get(position);
holder.itemView.setTag(item);
holder.text.setText(item.toString());
if (item instanceof DataBox.CookieModel && ((DataBox.CookieModel) item).isSelected() ||
item instanceof String && ((String) item).toLowerCase().endsWith(".zaai"))
holder.itemView.setBackgroundColor(0xF0_125687);
else
holder.itemView.setBackground(null);
}
@Override
public int getItemCount() {
return items != null ? items.size() : 0;
}
static final class SimpleViewHolder extends RecyclerView.ViewHolder {
private final TextView text;
private SimpleViewHolder(@NonNull final View itemView, final View.OnClickListener onClickListener,
final View.OnLongClickListener longClickListener) {
super(itemView);
text = itemView.findViewById(android.R.id.text1);
itemView.setOnClickListener(onClickListener);
if (longClickListener != null) itemView.setOnLongClickListener(longClickListener);
}
}
}

84
app/src/main/java/awais/instagrabber/adapters/StoriesAdapter.java

@ -0,0 +1,84 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.content.res.Resources;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import awais.instagrabber.R;
import awais.instagrabber.models.StoryModel;
public final class StoriesAdapter extends RecyclerView.Adapter<StoriesAdapter.StoryViewHolder> {
private final View.OnClickListener clickListener;
private LayoutInflater layoutInflater;
private StoryModel[] storyModels;
private Resources resources;
private int width, height;
public StoriesAdapter(final StoryModel[] storyModels, final View.OnClickListener clickListener) {
this.storyModels = storyModels;
this.clickListener = clickListener;
}
@NonNull
@Override
public StoryViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final Context context = parent.getContext();
if (layoutInflater == null) layoutInflater = LayoutInflater.from(context);
if (resources == null) resources = context.getResources();
height = Math.round(resources.getDimension(R.dimen.story_item_height));
width = Math.round(resources.getDimension(R.dimen.story_item_width));
return new StoryViewHolder(layoutInflater.inflate(R.layout.item_story, parent, false));
}
@Override
public void onBindViewHolder(@NonNull final StoryViewHolder holder, final int position) {
final StoryModel storyModel = storyModels[position];
if (storyModel != null) {
storyModel.setPosition(position);
holder.itemView.setTag(storyModel);
holder.itemView.setOnClickListener(clickListener);
holder.selectedView.setVisibility(storyModel.isCurrentSlide() ? View.VISIBLE : View.GONE);
Glide.with(holder.itemView).load(storyModel.getStoryUrl())
.apply(new RequestOptions().override(width, height))
.into(holder.icon);
}
}
public void setData(final StoryModel[] storyModels) {
this.storyModels = storyModels;
notifyDataSetChanged();
}
public StoryModel getItemAt(final int position) {
return storyModels == null ? null : storyModels[position];
}
@Override
public int getItemCount() {
return storyModels == null ? 0 : storyModels.length;
}
public final static class StoryViewHolder extends RecyclerView.ViewHolder {
public final ImageView icon, selectedView;
public StoryViewHolder(@NonNull final View itemView) {
super(itemView);
selectedView = itemView.findViewById(R.id.selectedView);
icon = itemView.findViewById(R.id.icon);
}
}
}

60
app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java

@ -0,0 +1,60 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.database.Cursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.cursoradapter.widget.CursorAdapter;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.request.RequestOptions;
import awais.instagrabber.R;
public final class SuggestionsAdapter extends CursorAdapter {
private final LayoutInflater layoutInflater;
private final View.OnClickListener onClickListener;
private final RequestManager glideRequestManager;
public SuggestionsAdapter(final Context context, final View.OnClickListener onClickListener) {
super(context, null, FLAG_REGISTER_CONTENT_OBSERVER);
this.glideRequestManager = Glide.with(context);
this.layoutInflater = LayoutInflater.from(context);
this.onClickListener = onClickListener;
}
@Override
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
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, picUrl, verified
// 0, 1 , 2 , 3 , 4 , 5
final String fullname = cursor.getString(2);
String username = cursor.getString(1);
final String picUrl = cursor.getString(4);
final boolean verified = cursor.getString(5).charAt(0) == 't';
if ("TYPE_HASHTAG".equals(cursor.getString(3))) username = '#' + username;
view.setOnClickListener(onClickListener);
view.setTag(username);
view.findViewById(R.id.isVerified).setVisibility(verified ? View.VISIBLE : View.GONE);
((TextView) view.findViewById(R.id.tvUsername)).setText(username);
((TextView) view.findViewById(R.id.tvFullName)).setText(fullname);
glideRequestManager.applyDefaultRequestOptions(new RequestOptions().skipMemoryCache(true))
.load(picUrl).into((ImageView) view.findViewById(R.id.ivProfilePic));
}
}

85
app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java

@ -0,0 +1,85 @@
package awais.instagrabber.adapters.viewholder;
import android.text.Spannable;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.CommentsAdapter;
import awais.instagrabber.customviews.RamboTextView;
import awais.instagrabber.interfaces.MentionClickListener;
import awais.instagrabber.models.CommentModel;
public final class CommentViewHolder extends RecyclerView.ViewHolder {
private final MentionClickListener mentionClickListener;
private final RecyclerView rvChildComments;
private final ImageView ivProfilePic;
private final TextView tvUsername, tvDate, tvComment, tvLikes;
private final View container;
public CommentViewHolder(@NonNull final View itemView, final View.OnClickListener onClickListener, final MentionClickListener mentionClickListener) {
super(itemView);
container = itemView.findViewById(R.id.container);
if (onClickListener != null) container.setOnClickListener(onClickListener);
this.mentionClickListener = mentionClickListener;
ivProfilePic = itemView.findViewById(R.id.ivProfilePic);
tvUsername = itemView.findViewById(R.id.tvUsername);
tvDate = itemView.findViewById(R.id.tvDate);
tvLikes = itemView.findViewById(R.id.tvLikes);
tvComment = itemView.findViewById(R.id.tvComment);
tvUsername.setSelected(true);
tvDate.setSelected(true);
rvChildComments = itemView.findViewById(R.id.rvChildComments);
}
public final ImageView getProfilePicView() {
return ivProfilePic;
}
public final boolean isParent() {
return rvChildComments != null;
}
public final void setCommentModel(final CommentModel commentModel) {
if (container != null) container.setTag(commentModel);
}
public final void setUsername(final String username) {
if (tvUsername != null) tvUsername.setText(username);
}
public final void setDate(final String date) {
if (tvDate != null) tvDate.setText(date);
}
public final void setLikes(final String likes) {
if (tvLikes != null) tvLikes.setText(likes);
}
public final void setCommment(final CharSequence commment) {
if (tvComment != null) {
tvComment.setText(commment, commment instanceof Spannable ? TextView.BufferType.SPANNABLE : TextView.BufferType.NORMAL);
((RamboTextView) tvComment).setMentionClickListener(mentionClickListener);
}
}
public final void setChildAdapter(final CommentsAdapter adapter) {
if (isParent()) {
rvChildComments.setAdapter(adapter);
rvChildComments.setVisibility(View.VISIBLE);
}
}
public final void hideChildComments() {
if (isParent()) rvChildComments.setVisibility(View.GONE);
}
}

43
app/src/main/java/awais/instagrabber/adapters/viewholder/DirectMessageViewHolder.java

@ -0,0 +1,43 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
public final class DirectMessageViewHolder extends RecyclerView.ViewHolder {
public final LinearLayout multipleProfilePicsContainer;
public final ImageView[] multipleProfilePics;
public final ImageView ivProfilePic, notTextType;
public final TextView tvUsername, tvDate, tvMessage;
public DirectMessageViewHolder(@NonNull final View itemView, final View.OnClickListener clickListener) {
super(itemView);
if (clickListener != null) itemView.setOnClickListener(clickListener);
itemView.findViewById(R.id.tvLikes).setVisibility(View.GONE);
tvDate = itemView.findViewById(R.id.tvDate);
tvMessage = itemView.findViewById(R.id.tvComment);
tvUsername = itemView.findViewById(R.id.tvUsername);
notTextType = itemView.findViewById(R.id.notTextType);
ivProfilePic = itemView.findViewById(R.id.ivProfilePic);
multipleProfilePicsContainer = itemView.findViewById(R.id.container);
final LinearLayout containerChild = (LinearLayout) multipleProfilePicsContainer.getChildAt(1);
multipleProfilePics = new ImageView[]{
(ImageView) multipleProfilePicsContainer.getChildAt(0),
(ImageView) containerChild.getChildAt(0),
(ImageView) containerChild.getChildAt(1)
};
tvDate.setSelected(true);
tvUsername.setSelected(true);
}
}

22
app/src/main/java/awais/instagrabber/adapters/viewholder/DiscoverViewHolder.java

@ -0,0 +1,22 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
public final class DiscoverViewHolder extends RecyclerView.ViewHolder {
public final ImageView postImage, typeIcon;
public final View selectedView, 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);
}
}

52
app/src/main/java/awais/instagrabber/adapters/viewholder/FeedItemViewHolder.java

@ -0,0 +1,52 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
import com.github.chrisbanes.photoview.PhotoView;
import com.google.android.exoplayer2.ui.PlayerView;
import awais.instagrabber.R;
import awais.instagrabber.customviews.RamboTextView;
public final class FeedItemViewHolder extends RecyclerView.ViewHolder {
public final ImageView profilePic, btnMute, btnDownload;
public final TextView username, commentsCount, videoViews, mediaCounter, tvPostDate;
public final RamboTextView viewerCaption;
public final View btnComments, videoViewsParent, viewPost;
public final ViewPager mediaList;
public final PhotoView imageView;
public final PlayerView playerView;
public FeedItemViewHolder(@NonNull final View itemView) {
super(itemView);
// common
viewerCaption = itemView.findViewById(R.id.viewerCaption);
btnDownload = itemView.findViewById(R.id.btnDownload);
btnComments = itemView.findViewById(R.id.btnComments);
profilePic = itemView.findViewById(R.id.ivProfilePic);
tvPostDate = itemView.findViewById(R.id.tvPostDate);
viewPost = itemView.findViewById(R.id.viewStoryPost);
username = itemView.findViewById(R.id.title);
// video view
btnMute = itemView.findViewById(R.id.btnMute);
videoViews = itemView.findViewById(R.id.tvVideoViews);
commentsCount = btnComments.findViewById(R.id.commentsCount);
videoViewsParent = videoViews != null ? (View) videoViews.getParent() : null;
// slider view
mediaCounter = itemView.findViewById(R.id.mediaCounter);
// different types
mediaList = itemView.findViewById(R.id.media_list);
imageView = itemView.findViewById(R.id.imageViewer);
playerView = itemView.findViewById(R.id.playerView);
}
}

22
app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java

@ -0,0 +1,22 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
public final class FollowsViewHolder extends RecyclerView.ViewHolder {
public final ImageView profileImage;
public final TextView tvFullName, tvUsername;
public FollowsViewHolder(@NonNull final View itemView) {
super(itemView);
profileImage = itemView.findViewById(R.id.ivProfilePic);
tvFullName = itemView.findViewById(R.id.tvFullName);
tvUsername = itemView.findViewById(R.id.tvUsername);
}
}

21
app/src/main/java/awais/instagrabber/adapters/viewholder/HighlightViewHolder.java

@ -0,0 +1,21 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
public final class HighlightViewHolder extends RecyclerView.ViewHolder {
public final ImageView icon;
public final TextView title;
public HighlightViewHolder(@NonNull final View itemView) {
super(itemView);
icon = itemView.findViewById(R.id.icon);
title = itemView.findViewById(R.id.title);
}
}

20
app/src/main/java/awais/instagrabber/adapters/viewholder/PostMediaViewHolder.java

@ -0,0 +1,20 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
public final class PostMediaViewHolder extends RecyclerView.ViewHolder {
public final ImageView icon, isDownloaded, selectedView;
public PostMediaViewHolder(@NonNull final View itemView) {
super(itemView);
selectedView = itemView.findViewById(R.id.selectedView);
isDownloaded = itemView.findViewById(R.id.isDownloaded);
icon = itemView.findViewById(R.id.icon);
}
}

23
app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewHolder.java

@ -0,0 +1,23 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
public final class PostViewHolder extends RecyclerView.ViewHolder {
public final ImageView postImage, typeIcon;
public final View selectedView, progressView, isDownloaded;
public PostViewHolder(@NonNull final View itemView) {
super(itemView);
typeIcon = itemView.findViewById(R.id.typeIcon);
postImage = itemView.findViewById(R.id.postImage);
isDownloaded = itemView.findViewById(R.id.isDownloaded);
selectedView = itemView.findViewById(R.id.selectedView);
progressView = itemView.findViewById(R.id.progressView);
}
}

91
app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/TextMessageViewHolder.java

@ -0,0 +1,91 @@
package awais.instagrabber.adapters.viewholder.directmessages;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.customviews.RamboTextView;
import awais.instagrabber.customviews.masoudss_waveform.WaveformSeekBar;
import awais.instagrabber.interfaces.MentionClickListener;
public final class TextMessageViewHolder extends RecyclerView.ViewHolder {
public final CardView rootCardView;
public final TextView tvUsername;
public final ImageView ivProfilePic;
// text message
public final RamboTextView tvMessage;
// expired message icon
public final View mediaExpiredIcon;
// media message
public final View mediaMessageContainer;
public final ImageView ivMediaPreview, mediaTypeIcon;
// profile messag
public final View profileMessageContainer, isVerified, btnOpenProfile;
public final TextView tvProfileUsername, tvProfileName;
public final ImageView ivMessageProfilePic;
// animated message
public final ImageView ivAnimatedMessage;
// link message
public final View linkMessageContainer;
public final ImageView ivLinkPreview;
public final TextView tvLinkTitle, tvLinkSummary;
// voice message
public final View voiceMessageContainer, btnPlayVoice;
public final WaveformSeekBar waveformSeekBar;
public final TextView tvVoiceDuration;
public TextMessageViewHolder(@NonNull final View itemView, final View.OnClickListener clickListener,
final MentionClickListener mentionClickListener) {
super(itemView);
if (clickListener != null) itemView.setOnClickListener(clickListener);
tvUsername = itemView.findViewById(R.id.tvUsername);
ivProfilePic = itemView.findViewById(R.id.ivProfilePic);
// text message
tvMessage = itemView.findViewById(R.id.tvMessage);
tvMessage.setCaptionIsExpandable(true);
tvMessage.setCaptionIsExpanded(true);
if (mentionClickListener != null) tvMessage.setMentionClickListener(mentionClickListener);
// root view
rootCardView = (CardView) tvMessage.getParent().getParent();
// expired message icon
mediaExpiredIcon = itemView.findViewById(R.id.mediaExpiredIcon);
// media message
ivMediaPreview = itemView.findViewById(R.id.ivMediaPreview);
mediaMessageContainer = (View) ivMediaPreview.getParent();
mediaTypeIcon = mediaMessageContainer.findViewById(R.id.typeIcon);
// profile message
btnOpenProfile = itemView.findViewById(R.id.btnInfo);
ivMessageProfilePic = itemView.findViewById(R.id.profileInfo);
profileMessageContainer = (View) ivMessageProfilePic.getParent();
isVerified = profileMessageContainer.findViewById(R.id.isVerified);
tvProfileName = profileMessageContainer.findViewById(R.id.tvFullName);
tvProfileUsername = profileMessageContainer.findViewById(R.id.profileInfoText);
// animated message
ivAnimatedMessage = itemView.findViewById(R.id.ivAnimatedMessage);
// link message
ivLinkPreview = itemView.findViewById(R.id.ivLinkPreview);
linkMessageContainer = (View) ivLinkPreview.getParent();
tvLinkTitle = linkMessageContainer.findViewById(R.id.tvLinkTitle);
tvLinkSummary = linkMessageContainer.findViewById(R.id.tvLinkSummary);
// voice message
waveformSeekBar = itemView.findViewById(R.id.waveformSeekBar);
voiceMessageContainer = (View) waveformSeekBar.getParent();
btnPlayVoice = voiceMessageContainer.findViewById(R.id.btnPlayVoice);
tvVoiceDuration = voiceMessageContainer.findViewById(R.id.tvVoiceDuration);
}
}

265
app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java

@ -0,0 +1,265 @@
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.Collections;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.CommentModel;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Utils.logCollector;
public final class CommentsFetcher extends AsyncTask<Void, Void, CommentModel[]> {
private final String shortCode;
private final FetchListener<CommentModel[]> fetchListener;
/*
* i fucking spent the whole day on this and fixing all the fucking problems in this class.
* DO NO FUCK WITH THIS CODE!
* -AWAiS (The Badak) @the.badak
*/
public CommentsFetcher(final String shortCode, final FetchListener<CommentModel[]> fetchListener) {
this.shortCode = shortCode;
this.fetchListener = fetchListener;
}
@NonNull
@Override
protected 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 ArrayList<CommentModel> commentModels = getParentComments();
for (final CommentModel commentModel : commentModels) {
final CommentModel[] childCommentModels = commentModel.getChildCommentModels();
if (childCommentModels != null) {
final int childCommentsLen = childCommentModels.length;
final CommentModel lastChild = childCommentModels[childCommentsLen - 1];
if (lastChild != null && lastChild.hasNextPage() && !Utils.isEmpty(lastChild.getEndCursor())) {
final CommentModel[] remoteChildComments = getChildComments(commentModel.getId());
commentModel.setChildCommentModels(remoteChildComments);
lastChild.setPageCursor(false, null);
}
}
}
return commentModels.toArray(new CommentModel[0]);
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final CommentModel[] result) {
if (fetchListener != null) fetchListener.onResult(result);
}
@NonNull
private synchronized CommentModel[] getChildComments(final String commentId) {
final ArrayList<CommentModel> commentModels = new ArrayList<>();
String endCursor = "";
while (endCursor != null) {
final String url = "https://www.instagram.com/graphql/query/?query_hash=51fdd02b67508306ad4484ff574a0b62&variables=" +
"{\"comment_id\":\"" + commentId + "\",\"first\":50,\"after\":\"" + endCursor + "\"}";
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(Utils.readFromConnection(conn)).getJSONObject("data")
.getJSONObject("comment").getJSONObject("edge_threaded_comments");
final JSONObject pageInfo = data.getJSONObject("page_info");
endCursor = pageInfo.getString("end_cursor");
if (Utils.isEmpty(endCursor)) endCursor = 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 ProfileModel profileModel = new ProfileModel(false,
false,
owner.getString(Constants.EXTRAS_ID),
owner.getString(Constants.EXTRAS_USERNAME),
null, null, null,
owner.getString("profile_pic_url"),
null, 0, 0, 0);
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,
profileModel));
}
}
}
}
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("AWAISKING_APP", "", e);
break;
}
}
return commentModels.toArray(new CommentModel[0]);
}
@NonNull
private synchronized ArrayList<CommentModel> getParentComments() {
final ArrayList<CommentModel> commentModelsList = new ArrayList<>();
String endCursor = "";
while (endCursor != null) {
final String url = "https://www.instagram.com/graphql/query/?query_hash=97b41c52301f77ce508f55e66d17620e&variables=" +
"{\"shortcode\":\"" + shortCode + "\",\"first\":50,\"after\":\"" + endCursor + "\"}";
try {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) break;
else {
final JSONObject parentComments = new JSONObject(Utils.readFromConnection(conn)).getJSONObject("data")
.getJSONObject("shortcode_media").getJSONObject("edge_media_to_parent_comment");
final JSONObject pageInfo = parentComments.getJSONObject("page_info");
endCursor = pageInfo.optString("end_cursor");
if (Utils.isEmpty(endCursor)) endCursor = null;
// 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();
final CommentModel[] commentModels = new CommentModel[commentsLen];
for (int i = 0; i < commentsLen; ++i) {
final JSONObject comment = comments.getJSONObject(i).getJSONObject("node");
final JSONObject owner = comment.getJSONObject("owner");
final ProfileModel profileModel = new ProfileModel(false,
owner.optBoolean("is_verified"),
owner.getString(Constants.EXTRAS_ID),
owner.getString(Constants.EXTRAS_USERNAME),
null, null, null,
owner.getString("profile_pic_url"),
null, 0, 0, 0);
final JSONObject likedBy = comment.optJSONObject("edge_liked_by");
final String commentId = comment.getString(Constants.EXTRAS_ID);
commentModels[i] = new CommentModel(commentId,
comment.getString("text"),
comment.getLong("created_at"),
likedBy != null ? likedBy.optLong("count", 0) : 0,
profileModel);
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 hasNextPage;
if ((tempJsonObject = tempJsonObject.optJSONObject("page_info")) != null) {
childEndCursor = tempJsonObject.optString("end_cursor");
hasNextPage = tempJsonObject.optBoolean("has_next_page", !Utils.isEmpty(childEndCursor));
} else {
childEndCursor = null;
hasNextPage = false;
}
final CommentModel[] childCommentModels = new CommentModel[childCommentsLen];
for (int j = 0; j < childCommentsLen; ++j) {
final JSONObject childComment = childCommentsArray.getJSONObject(j).getJSONObject("node");
tempJsonObject = childComment.getJSONObject("owner");
final ProfileModel childProfileModel = new ProfileModel(false, false,
tempJsonObject.getString(Constants.EXTRAS_ID),
tempJsonObject.getString(Constants.EXTRAS_USERNAME),
null, null, null,
tempJsonObject.getString("profile_pic_url"),
null, 0, 0, 0);
tempJsonObject = childComment.optJSONObject("edge_liked_by");
childCommentModels[j] = new CommentModel(childComment.getString(Constants.EXTRAS_ID),
childComment.getString("text"),
childComment.getLong("created_at"),
tempJsonObject != null ? tempJsonObject.optLong("count", 0) : 0,
childProfileModel);
}
childCommentModels[childCommentsLen - 1].setPageCursor(hasNextPage, childEndCursor);
commentModels[i].setChildCommentModels(childCommentModels);
}
}
Collections.addAll(commentModelsList, commentModels);
}
conn.disconnect();
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_COMMENTS_FETCHER, "getParentComments",
new Pair<>("commentModelsList.size", commentModelsList.size()));
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
break;
}
}
return commentModelsList;
}
}

194
app/src/main/java/awais/instagrabber/asyncs/DiscoverFetcher.java

@ -0,0 +1,194 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.DiscoverItemModel;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Constants.FOLDER_PATH;
import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO;
import static awais.instagrabber.utils.Utils.logCollector;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class DiscoverFetcher extends AsyncTask<Void, Void, DiscoverItemModel[]> {
private final String maxId;
private final FetchListener<DiscoverItemModel[]> fetchListener;
private int lastId = 0;
private boolean isFirst, moreAvailable;
private String nextMaxId;
public DiscoverFetcher(final String maxId, final FetchListener<DiscoverItemModel[]> fetchListener, final boolean isFirst) {
this.maxId = maxId == null ? "" : "&max_id=" + maxId;
this.fetchListener = fetchListener;
this.isFirst = isFirst;
}
@Nullable
@Override
protected final DiscoverItemModel[] doInBackground(final Void... voids) {
// to check if file exists
final File downloadDir = new File(Environment.getExternalStorageDirectory(), "Download");
File customDir = null;
if (settingsHelper.getBoolean(FOLDER_SAVE_TO)) {
final String customPath = settingsHelper.getString(FOLDER_PATH);
if (!Utils.isEmpty(customPath)) customDir = new File(customPath);
}
DiscoverItemModel[] result = null;
final ArrayList<DiscoverItemModel> discoverItemModels = fetchItems(downloadDir, customDir, null, maxId);
if (discoverItemModels != null) {
result = discoverItemModels.toArray(new DiscoverItemModel[0]);
if (result.length > 0) {
final DiscoverItemModel lastModel = result[result.length - 1];
if (lastModel != null) lastModel.setMore(moreAvailable, nextMaxId);
}
}
return result;
}
private ArrayList<DiscoverItemModel> fetchItems(final File downloadDir, final File customDir,
ArrayList<DiscoverItemModel> discoverItemModels, final String maxId) {
try {
final String url = "https://www.instagram.com/explore/grid/?is_prefetch=false&omit_cover_media=true&module=explore_popular" +
"&use_sectional_payload=false&cluster_id=explore_all%3A0&include_fixed_destinations=true" + maxId;
final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
urlConnection.setUseCaches(false);
urlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 8.1.0; motorola one Build/OPKS28.63-18-3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/70.0.3538.80 Mobile Safari/537.36 Instagram 72.0.0.21.98 Android (27/8.1.0; 320dpi; 720x1362; motorola; motorola one; deen_sprout; qcom; pt_BR; 132081645)");
if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
final JSONObject discoverResponse = new JSONObject(Utils.readFromConnection(urlConnection));
moreAvailable = discoverResponse.getBoolean("more_available");
nextMaxId = discoverResponse.getString("next_max_id");
final JSONArray sectionalItems = discoverResponse.getJSONArray("sectional_items");
if (discoverItemModels == null) discoverItemModels = new ArrayList<>(sectionalItems.length() * 2);
for (int i = 0; i < sectionalItems.length(); ++i) {
final JSONObject sectionItem = sectionalItems.getJSONObject(i);
final String feedType = sectionItem.getString("feed_type");
final String layoutType = sectionItem.getString("layout_type");
if (sectionItem.has("layout_content") && feedType.equals("media")) {
final JSONObject layoutContent = sectionItem.getJSONObject("layout_content");
if ("media_grid".equals(layoutType)) {
final JSONArray medias = layoutContent.getJSONArray("medias");
for (int j = 0; j < medias.length(); ++j)
discoverItemModels.add(makeDiscoverModel(downloadDir, customDir,
medias.getJSONObject(j).getJSONObject("media")));
} else {
final boolean isOneSide = "one_by_two_left".equals(layoutType);
if (isOneSide || "two_by_two_right".equals(layoutType)) {
final JSONObject layoutItem = layoutContent.getJSONObject(isOneSide ? "one_by_two_item" : "two_by_two_item");
if (layoutItem.has("media"))
discoverItemModels.add(makeDiscoverModel(downloadDir, customDir,
layoutItem.getJSONObject("media")));
if (layoutContent.has("fill_items")) {
final JSONArray fillItems = layoutContent.getJSONArray("fill_items");
for (int j = 0; j < fillItems.length(); ++j)
discoverItemModels.add(makeDiscoverModel(downloadDir, customDir,
fillItems.getJSONObject(j).getJSONObject("media")));
}
}
}
}
}
discoverItemModels.trimToSize();
urlConnection.disconnect();
// hack to fetch 50+ items
if (this.isFirst) {
final int size = discoverItemModels.size();
if (size > 50) this.isFirst = false;
discoverItemModels = fetchItems(downloadDir, customDir, discoverItemModels,
"&max_id=" + (lastId++));
}
} else {
urlConnection.disconnect();
}
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_DISCOVER_FETCHER, "fetchItems",
new Pair<>("maxId", maxId),
new Pair<>("lastId", lastId),
new Pair<>("isFirst", isFirst),
new Pair<>("nextMaxId", nextMaxId));
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return discoverItemModels;
}
@NonNull
private DiscoverItemModel makeDiscoverModel(final File downloadDir, final File customDir,
@NonNull final JSONObject media) throws Exception {
final JSONObject user = media.getJSONObject(Constants.EXTRAS_USER);
final String username = user.getString(Constants.EXTRAS_USERNAME);
// final ProfileModel userProfileModel = new ProfileModel(user.getBoolean("is_private"),
// user.getBoolean("is_verified"),
// String.valueOf(user.get("pk")),
// username,
// user.getString("full_name"),
// null,
// user.getString("profile_pic_url"), null,
// 0, 0, 0);
// final String comment;
// if (!media.has("caption")) comment = null;
// else {
// final Object caption = media.get("caption");
// comment = caption instanceof JSONObject ? ((JSONObject) caption).getString("text") : null;
// }
final MediaItemType mediaType = Utils.getMediaItemType(media.getInt("media_type"));
final DiscoverItemModel model = new DiscoverItemModel(mediaType,
media.getString(Constants.EXTRAS_ID),
media.getString("code"),
Utils.getThumbnailUrl(media, mediaType));
Utils.checkExistence(downloadDir, customDir, username,
mediaType == MediaItemType.MEDIA_TYPE_SLIDER, -1, model);
return model;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final DiscoverItemModel[] discoverItemModels) {
if (fetchListener != null) fetchListener.onResult(discoverItemModels);
}
}

248
app/src/main/java/awais/instagrabber/asyncs/DownloadAsync.java

@ -0,0 +1,248 @@
package awais.instagrabber.asyncs;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaMetadataRetriever;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.FileProvider;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.atomic.AtomicReference;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.R;
import awais.instagrabber.activities.ProfileViewer;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.CHANNEL_ID;
import static awais.instagrabber.utils.Utils.CHANNEL_NAME;
import static awais.instagrabber.utils.Utils.NOTIF_GROUP_NAME;
import static awais.instagrabber.utils.Utils.isChannelCreated;
import static awais.instagrabber.utils.Utils.logCollector;
import static awais.instagrabber.utils.Utils.notificationManager;
import static awaisomereport.LogCollector.LogFile;
public final class DownloadAsync extends AsyncTask<Void, Float, File> {
private static int lastNotifId = 1;
private final int currentNotifId;
private final AtomicReference<Context> context;
private final File outFile;
private final String url;
private final FetchListener<File> fetchListener;
private final Resources resources;
private final NotificationCompat.Builder downloadNotif;
private String shortCode, username;
public DownloadAsync(final Context context, final String url, final File outFile, final FetchListener<File> fetchListener) {
this.context = new AtomicReference<>(context);
this.resources = context.getResources();
this.url = url;
this.outFile = outFile;
this.fetchListener = fetchListener;
this.shortCode = this.username = resources.getString(R.string.downloader_started);
this.currentNotifId = ++lastNotifId;
if (++lastNotifId + 1 == Integer.MAX_VALUE) lastNotifId = 1;
if (notificationManager == null)
notificationManager = NotificationManagerCompat.from(context.getApplicationContext());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isChannelCreated) {
notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH));
isChannelCreated = true;
}
@StringRes final int titleRes = context instanceof ProfileViewer ? R.string.downloader_downloading_pfp : R.string.downloader_downloading_post;
downloadNotif = new NotificationCompat.Builder(context, CHANNEL_ID).setCategory(NotificationCompat.CATEGORY_STATUS)
.setSmallIcon(R.mipmap.ic_launcher).setContentText(shortCode == null ? username : shortCode).setOngoing(true)
.setProgress(100, 0, false).setAutoCancel(false).setOnlyAlertOnce(true)
.setContentTitle(resources.getString(titleRes));
notificationManager.notify(currentNotifId, downloadNotif.build());
}
public DownloadAsync setItems(final String shortCode, final String username) {
this.shortCode = shortCode;
this.username = username;
if (downloadNotif != null) downloadNotif.setContentText(this.shortCode == null ? this.username : this.shortCode);
return this;
}
@Nullable
@Override
protected File doInBackground(final Void... voids) {
try {
final URLConnection urlConnection = new URL(url).openConnection();
final long fileSize = Build.VERSION.SDK_INT >= 24 ? urlConnection.getContentLengthLong() :
urlConnection.getContentLength();
float totalRead = 0;
try (final BufferedInputStream bis = new BufferedInputStream(urlConnection.getInputStream());
final FileOutputStream fos = new FileOutputStream(outFile)) {
final byte[] buffer = new byte[0x2000];
int count;
boolean deletedIPTC = false;
while ((count = bis.read(buffer, 0, 0x2000)) != -1) {
totalRead = totalRead + count;
if (!deletedIPTC) {
int iptcStart = -1;
int fbmdStart = -1;
int fbmdBytesLen = -1;
for (int i = 0; i < buffer.length; ++i) {
if (buffer[i] == (byte) 0xFF && buffer[i + 1] == (byte) 0xED)
iptcStart = i;
else if (buffer[i] == (byte) 'F' && buffer[i + 1] == (byte) 'B'
&& buffer[i + 2] == (byte) 'M' && buffer[i + 3] == (byte) 'D') {
fbmdStart = i;
fbmdBytesLen = buffer[i - 10] << 24 | (buffer[i - 9] & 0xFF) << 16 |
(buffer[i - 8] & 0xFF) << 8 | (buffer[i - 7] & 0xFF) |
(buffer[i - 6] & 0xFF);
break;
}
}
if (iptcStart != -1 && fbmdStart != -1 && fbmdBytesLen != -1) {
final int fbmdDataLen = (iptcStart + (fbmdStart - iptcStart) + (fbmdBytesLen - iptcStart)) - 4;
fos.write(buffer, 0, iptcStart);
fos.write(buffer, fbmdDataLen + iptcStart, count - fbmdDataLen - iptcStart);
publishProgress(totalRead * 100f / fileSize);
deletedIPTC = true;
continue;
}
}
fos.write(buffer, 0, count);
publishProgress(totalRead * 100f / fileSize);
}
fos.flush();
}
return outFile;
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogFile.ASYNC_DOWNLOADER, "doInBackground",
new Pair<>("context", context.get()),
new Pair<>("resources", resources),
new Pair<>("lastNotifId", lastNotifId),
new Pair<>("downloadNotif", downloadNotif),
new Pair<>("currentNotifId", currentNotifId),
new Pair<>("notificationManager", notificationManager));
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return null;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onProgressUpdate(@NonNull final Float... values) {
if (downloadNotif != null) {
downloadNotif.setProgress(100, values[0].intValue(), false);
notificationManager.notify(currentNotifId, downloadNotif.build());
}
}
@Override
protected void onPostExecute(final File result) {
if (result != null) {
final Context context = this.context.get();
context.sendBroadcast(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT ?
new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.fromFile(Environment.getExternalStorageDirectory())) :
new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(result.getAbsoluteFile()))
);
MediaScannerConnection.scanFile(context, new String[]{result.getAbsolutePath()}, null, null);
if (notificationManager != null) {
final Uri uri = FileProvider.getUriForFile(context, "awais.instagrabber.provider", result);
final ContentResolver contentResolver = context.getContentResolver();
Bitmap bitmap = null;
if (Utils.isImage(uri, contentResolver)) {
try {
bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri));
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_1");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
}
if (bitmap == null) {
final MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
try {
retriever.setDataSource(context, uri);
} catch (final Exception e) {
retriever.setDataSource(result.getAbsolutePath());
}
bitmap = retriever.getFrameAtTime();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
try {
retriever.close();
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_2");
}
} catch (final Exception e) {
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
if (logCollector != null)
logCollector.appendException(e, LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_3");
}
}
final String downloadComplete = resources.getString(R.string.downloader_complete);
downloadNotif.setContentText(null).setContentTitle(downloadComplete).setProgress(0, 0, false)
.setWhen(System.currentTimeMillis()).setOngoing(false).setOnlyAlertOnce(false).setAutoCancel(true)
.setGroup(NOTIF_GROUP_NAME).setGroupSummary(true).setContentIntent(
PendingIntent.getActivity(context, 2020, new Intent(Intent.ACTION_VIEW, uri)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_FROM_BACKGROUND | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.putExtra(Intent.EXTRA_STREAM, uri), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT));
if (bitmap != null)
downloadNotif.setStyle(new NotificationCompat.BigPictureStyle().setBigContentTitle(downloadComplete).bigPicture(bitmap))
.setLargeIcon(bitmap).setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL);
notificationManager.cancel(currentNotifId);
notificationManager.notify(currentNotifId + 1, downloadNotif.build());
}
}
if (fetchListener != null) fetchListener.onResult(result);
}
}

194
app/src/main/java/awais/instagrabber/asyncs/FeedFetcher.java

@ -0,0 +1,194 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.models.ViewerPostModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Utils.logCollector;
public final class FeedFetcher extends AsyncTask<Void, Void, FeedModel[]> {
private static final int maxItemsToLoad = 25; // max is 50, but that's too many posts, setting more than 30 is gay
private final String endCursor;
private final FetchListener<FeedModel[]> fetchListener;
public FeedFetcher(final FetchListener<FeedModel[]> fetchListener) {
this.endCursor = "";
this.fetchListener = fetchListener;
}
public FeedFetcher(final String endCursor, final FetchListener<FeedModel[]> fetchListener) {
this.endCursor = endCursor == null ? "" : endCursor;
this.fetchListener = fetchListener;
}
@Nullable
@Override
protected final FeedModel[] doInBackground(final Void... voids) {
FeedModel[] result = null;
try {
//
// stories: 04334405dbdef91f2c4e207b84c204d7 && https://i.instagram.com/api/v1/feed/reels_tray/
// https://www.instagram.com/graphql/query/?query_hash=04334405dbdef91f2c4e207b84c204d7&variables={"only_stories":true,"stories_prefetch":false,"stories_video_dash_manifest":false}
// ///////////////////////////////////////////////
// feed:
// https://www.instagram.com/graphql/query/?query_hash=6b838488258d7a4820e48d209ef79eb1&variables=
// {"cached_feed_item_ids":[],"fetch_media_item_count":12,"fetch_media_item_cursor":"<end_cursor>","fetch_comment_count":4,"fetch_like":3,"has_stories":false,"has_threaded_comments":true}
// only used: fetch_media_item_cursor, fetch_media_item_count: 100 (max 50), has_threaded_comments = true
// //////////////////////////////////////////////
// more unknowns: https://github.com/qsniyg/rssit/blob/master/rssit/generators/instagram.py
//
final String url = "https://www.instagram.com/graphql/query/?query_hash=6b838488258d7a4820e48d209ef79eb1&variables=" +
"{\"fetch_media_item_count\":" + maxItemsToLoad + ",\"has_threaded_comments\":true,\"fetch_media_item_cursor\":\"" + endCursor + "\"}";
final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
final JSONObject timelineFeed = new JSONObject(Utils.readFromConnection(urlConnection)).getJSONObject("data")
.getJSONObject(Constants.EXTRAS_USER).getJSONObject("edge_web_feed_timeline");
final String endCursor;
final boolean hasNextPage;
final JSONObject pageInfo = timelineFeed.getJSONObject("page_info");
if (pageInfo.has("has_next_page")) {
hasNextPage = pageInfo.getBoolean("has_next_page");
endCursor = hasNextPage ? pageInfo.getString("end_cursor") : null;
} else {
hasNextPage = false;
endCursor = null;
}
final JSONArray feedItems = timelineFeed.getJSONArray("edges");
final int feedLen = feedItems.length();
final ArrayList<FeedModel> feedModelsList = new ArrayList<>(feedLen);
for (int i = 0; i < feedLen; ++i) {
final JSONObject feedItem = feedItems.getJSONObject(i).getJSONObject("node");
final String mediaType = feedItem.optString("__typename");
if (mediaType.isEmpty() || "GraphSuggestedUserFeedUnit".equals(mediaType)) continue;
final boolean isVideo = feedItem.optBoolean("is_video");
final long videoViews = feedItem.optLong("video_view_count", 0);
final String displayUrl = feedItem.getString("display_url");
final String resourceUrl;
if (isVideo) resourceUrl = feedItem.getString("video_url");
else resourceUrl = feedItem.has("display_resources") ? Utils.getHighQualityImage(feedItem) : displayUrl;
ProfileModel profileModel = null;
if (feedItem.has("owner")) {
final JSONObject owner = feedItem.getJSONObject("owner");
profileModel = new ProfileModel(owner.optBoolean("is_private"),
owner.optBoolean("is_verified"),
owner.getString(Constants.EXTRAS_ID),
owner.getString(Constants.EXTRAS_USERNAME),
owner.optString("full_name"),
null, null,
owner.getString("profile_pic_url"),
null, 0, 0, 0);
}
JSONObject tempJsonObject = feedItem.optJSONObject("edge_media_preview_comment");
final long commentsCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0;
tempJsonObject = feedItem.optJSONObject("edge_media_to_caption");
final JSONArray captions = tempJsonObject != null ? tempJsonObject.getJSONArray("edges") : null;
String captionText = null;
if (captions != null && captions.length() > 0) {
if ((tempJsonObject = captions.optJSONObject(0)) != null &&
(tempJsonObject = tempJsonObject.optJSONObject("node")) != null)
captionText = tempJsonObject.getString("text");
}
final FeedModel feedModel = new FeedModel(profileModel,
isVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE,
videoViews,
feedItem.getString(Constants.EXTRAS_ID),
resourceUrl,
displayUrl,
feedItem.getString(Constants.EXTRAS_SHORTCODE),
captionText,
commentsCount,
feedItem.optLong("taken_at_timestamp", -1));
final boolean isSlider = "GraphSidecar".equals(mediaType) && feedItem.has("edge_sidecar_to_children");
if (isSlider) {
final JSONObject sidecar = feedItem.optJSONObject("edge_sidecar_to_children");
if (sidecar != null) {
final JSONArray children = sidecar.optJSONArray("edges");
if (children != null) {
final ViewerPostModel[] sliderItems = new ViewerPostModel[children.length()];
for (int j = 0; j < sliderItems.length; ++j) {
final JSONObject node = children.optJSONObject(j).getJSONObject("node");
final boolean isChildVideo = node.optBoolean("is_video");
sliderItems[j] = new ViewerPostModel(
isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE,
node.getString(Constants.EXTRAS_ID),
isChildVideo ? node.getString("video_url") : Utils.getHighQualityImage(node),
null, null, null,
node.optLong("video_view_count", -1), -1);
sliderItems[j].setSliderDisplayUrl(node.getString("display_url"));
}
feedModel.setSliderItems(sliderItems);
}
}
}
feedModelsList.add(feedModel);
}
feedModelsList.trimToSize();
final FeedModel[] feedModels = feedModelsList.toArray(new FeedModel[0]);
if (feedModels[feedModels.length - 1] != null)
feedModels[feedModels.length - 1].setPageCursor(hasNextPage, endCursor);
result = feedModels;
}
urlConnection.disconnect();
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_FEED_FETCHER, "doInBackground");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final FeedModel[] postModels) {
if (fetchListener != null) fetchListener.onResult(postModels);
}
}

103
app/src/main/java/awais/instagrabber/asyncs/FeedStoriesFetcher.java

@ -0,0 +1,103 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.FeedStoryModel;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector.LogFile;
import static awais.instagrabber.utils.Utils.logCollector;
public final class FeedStoriesFetcher extends AsyncTask<Void, Void, FeedStoryModel[]> {
private final FetchListener<FeedStoryModel[]> fetchListener;
public FeedStoriesFetcher(final FetchListener<FeedStoryModel[]> fetchListener) {
this.fetchListener = fetchListener;
}
@Override
protected FeedStoryModel[] doInBackground(final Void... voids) {
FeedStoryModel[] result = null;
String url = "https://www.instagram.com/graphql/query/?query_hash=b7b84d884400bc5aa7cfe12ae843a091&variables=" +
"{\"only_stories\":true,\"stories_prefetch\":false,\"stories_video_dash_manifest\":false}";
try {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setInstanceFollowRedirects(false);
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
final JSONArray feedStoriesReel = new JSONObject(Utils.readFromConnection(conn))
.getJSONObject("data")
.getJSONObject(Constants.EXTRAS_USER)
.getJSONObject("feed_reels_tray")
.getJSONObject("edge_reels_tray_to_reel")
.getJSONArray("edges");
conn.disconnect();
final int storiesLen = feedStoriesReel.length();
final FeedStoryModel[] feedStoryModels = new FeedStoryModel[storiesLen];
final String[] feedStoryIDs = new String[storiesLen];
for (int i = 0; i < storiesLen; ++i) {
final JSONObject node = feedStoriesReel.getJSONObject(i).getJSONObject("node");
final JSONObject user = node.getJSONObject(node.has("user") ? "user" : "owner");
final ProfileModel profileModel = new ProfileModel(false, false,
user.getString("id"),
user.getString("username"),
null, null, null,
user.getString("profile_pic_url"),
null, 0, 0, 0);
final String id = node.getString("id");
feedStoryIDs[i] = id;
feedStoryModels[i] = new FeedStoryModel(id, profileModel);
}
url = "https://www.instagram.com/graphql/query/?query_hash=0a85e6ea60a4c99edc58ab2f3d17cfdf&variables=" +
"{\"reel_ids\":" + Utils.highlightIdsMerger(feedStoryIDs) + ",\"precomposed_overlay\":false}";
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setInstanceFollowRedirects(false);
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
Utils.putHighlightModels(conn, feedStoryModels);
}
result = feedStoryModels;
}
conn.disconnect();
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogFile.ASYNC_FEED_STORY_FETCHER, "doInBackground");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final FeedStoryModel[] result) {
if (fetchListener != null) fetchListener.onResult(result);
}
}

101
app/src/main/java/awais/instagrabber/asyncs/FollowFetcher.java

@ -0,0 +1,101 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Utils.logCollector;
public final class FollowFetcher extends AsyncTask<Void, Void, FollowModel[]> {
private final String endCursor, id;
private final boolean isFollowers;
private final FetchListener<FollowModel[]> fetchListener;
public FollowFetcher(final String id, final boolean isFollowers, final FetchListener<FollowModel[]> fetchListener) {
this.id = id;
this.endCursor = "";
this.isFollowers = isFollowers;
this.fetchListener = fetchListener;
}
public FollowFetcher(final String id, final boolean isFollowers, final String endCursor, final FetchListener<FollowModel[]> fetchListener) {
this.id = id;
this.endCursor = endCursor == null ? "" : endCursor;
this.isFollowers = isFollowers;
this.fetchListener = fetchListener;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected FollowModel[] doInBackground(final Void... voids) {
FollowModel[] result = null;
final String url = "https://www.instagram.com/graphql/query/?query_id=" + (isFollowers ? "17851374694183129" : "17874545323001329")
+ "&id=" + id + "&first=50&after=" + endCursor;
try {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setInstanceFollowRedirects(false);
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
final JSONObject data = new JSONObject(Utils.readFromConnection(conn)).getJSONObject("data")
.getJSONObject(Constants.EXTRAS_USER).getJSONObject(isFollowers ? "edge_followed_by" : "edge_follow");
final String endCursor;
final boolean hasNextPage;
final JSONObject pageInfo = data.getJSONObject("page_info");
if (pageInfo.has("has_next_page")) {
hasNextPage = pageInfo.getBoolean("has_next_page");
endCursor = hasNextPage ? pageInfo.getString("end_cursor") : null;
} else {
hasNextPage = false;
endCursor = null;
}
final JSONArray edges = data.getJSONArray("edges");
final FollowModel[] models = new FollowModel[edges.length()];
for (int i = 0; i < models.length; ++i) {
final JSONObject followNode = edges.getJSONObject(i).getJSONObject("node");
models[i] = new FollowModel(followNode.getString(Constants.EXTRAS_ID), followNode.getString(Constants.EXTRAS_USERNAME),
followNode.getString("full_name"), followNode.getString("profile_pic_url"));
}
if (models[models.length - 1] != null)
models[models.length - 1].setPageCursor(hasNextPage, endCursor);
result = models;
}
conn.disconnect();
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_FOLLOW_FETCHER, "doInBackground");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPostExecute(final FollowModel[] result) {
if (fetchListener != null) fetchListener.onResult(result);
}
}

87
app/src/main/java/awais/instagrabber/asyncs/HighlightsFetcher.java

@ -0,0 +1,87 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.HighlightModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
public final class HighlightsFetcher extends AsyncTask<Void, Void, HighlightModel[]> {
private final String id;
private final FetchListener<HighlightModel[]> fetchListener;
public HighlightsFetcher(final String id, final FetchListener<HighlightModel[]> fetchListener) {
this.id = id;
this.fetchListener = fetchListener;
}
@Override
protected HighlightModel[] doInBackground(final Void... voids) {
HighlightModel[] result = null;
String url = "https://www.instagram.com/graphql/query/?query_hash=7c16654f22c819fb63d1183034a5162f&variables=" +
"{\"user_id\":\"" + id + "\",\"include_chaining\":false,\"include_reel\":true,\"include_suggested_users\":false," +
"\"include_logged_out_extras\":false,\"include_highlight_reels\":true}";
try {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setInstanceFollowRedirects(false);
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
final JSONArray highlightsReel = new JSONObject(Utils.readFromConnection(conn)).getJSONObject("data")
.getJSONObject(Constants.EXTRAS_USER).getJSONObject("edge_highlight_reels").getJSONArray("edges");
final int length = highlightsReel.length();
final HighlightModel[] highlightModels = new HighlightModel[length];
final String[] highlightIds = new String[length];
for (int i = 0; i < length; ++i) {
final JSONObject highlightNode = highlightsReel.getJSONObject(i).getJSONObject("node");
final String id = highlightNode.getString(Constants.EXTRAS_ID);
highlightIds[i] = id;
highlightModels[i] = new HighlightModel(
highlightNode.getString("title"),
highlightNode.getJSONObject("cover_media").getString("thumbnail_src")
);
}
conn.disconnect();
// a22a50ce4582220909e302d6eb84d259
// 45246d3fe16ccc6577e0bd297a5db1ab
url = "https://www.instagram.com/graphql/query/?query_hash=a22a50ce4582220909e302d6eb84d259&variables=" +
"{\"highlight_reel_ids\":" + Utils.highlightIdsMerger(highlightIds) + ",\"reel_ids\":[],\"location_ids\":[],\"precomposed_overlay\":false}";
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setInstanceFollowRedirects(false);
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
Utils.putHighlightModels(conn, highlightModels);
}
result = highlightModels;
}
conn.disconnect();
} catch (Exception e) {
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPostExecute(final HighlightModel[] result) {
if (fetchListener != null) fetchListener.onResult(result);
}
}

146
app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java

@ -0,0 +1,146 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.models.ViewerPostModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Constants.FOLDER_PATH;
import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO;
import static awais.instagrabber.utils.Utils.logCollector;
public final class PostFetcher extends AsyncTask<Void, Void, ViewerPostModel[]> {
private final String shortCode;
private final FetchListener<ViewerPostModel[]> fetchListener;
public PostFetcher(final String shortCode, final FetchListener<ViewerPostModel[]> fetchListener) {
this.shortCode = shortCode;
this.fetchListener = fetchListener;
}
@Override
protected ViewerPostModel[] doInBackground(final Void... voids) {
ViewerPostModel[] result = null;
try {
final HttpURLConnection conn = (HttpURLConnection) new URL("https://www.instagram.com/p/" + shortCode + "/?__a=1").openConnection();
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
// to check if file exists
final File downloadDir = new File(Environment.getExternalStorageDirectory(), "Download");
File customDir = null;
if (Utils.settingsHelper.getBoolean(FOLDER_SAVE_TO)) {
final String customPath = Utils.settingsHelper.getString(FOLDER_PATH);
if (!Utils.isEmpty(customPath)) customDir = new File(customPath);
}
final JSONObject media = new JSONObject(Utils.readFromConnection(conn)).getJSONObject("graphql")
.getJSONObject("shortcode_media");
final String username = media.has("owner") ? media.getJSONObject("owner").getString(Constants.EXTRAS_USERNAME) : null;
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;
String endCursor = null;
if (commentObject != null && (commentObject = commentObject.optJSONObject("page_info")) != null)
endCursor = commentObject.optString("end_cursor");
if (mediaItemType != MediaItemType.MEDIA_TYPE_SLIDER) {
final ViewerPostModel postModel = new ViewerPostModel(mediaItemType,
media.getString(Constants.EXTRAS_ID),
isVideo ? media.getString("video_url") : Utils.getHighQualityImage(media),
shortCode,
Utils.isEmpty(postCaption) ? null : postCaption,
username,
isVideo && media.has("video_view_count") ? media.getLong("video_view_count") : -1,
timestamp);
postModel.setCommentsCount(commentsCount);
postModel.setCommentsEndCursor(endCursor);
Utils.checkExistence(downloadDir, customDir, username, false, -1, postModel);
result = new ViewerPostModel[]{postModel};
} else {
final JSONArray children = media.getJSONObject("edge_sidecar_to_children").getJSONArray("edges");
final ViewerPostModel[] postModels = new ViewerPostModel[children.length()];
for (int i = 0; i < postModels.length; ++i) {
final JSONObject node = children.getJSONObject(i).getJSONObject("node");
final boolean isChildVideo = node.getBoolean("is_video");
postModels[i] = new ViewerPostModel(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE,
node.getString(Constants.EXTRAS_ID),
isChildVideo ? node.getString("video_url") : Utils.getHighQualityImage(node),
node.getString(Constants.EXTRAS_SHORTCODE),
postCaption,
username,
isChildVideo && node.has("video_view_count") ? node.getLong("video_view_count") : -1,
timestamp);
postModels[i].setSliderDisplayUrl(node.getString("display_url"));
Utils.checkExistence(downloadDir, customDir, username, true, i, postModels[i]);
}
postModels[0].setCommentsCount(commentsCount);
postModels[0].setCommentsEndCursor(endCursor);
result = postModels;
}
}
conn.disconnect();
} catch (Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_POST_FETCHER, "doInBackground");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final ViewerPostModel[] postModels) {
if (fetchListener != null) fetchListener.onResult(postModels);
}
}

134
app/src/main/java/awais/instagrabber/asyncs/PostsFetcher.java

@ -0,0 +1,134 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.models.PostModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Constants.FOLDER_PATH;
import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO;
import static awais.instagrabber.utils.Utils.logCollector;
public final class PostsFetcher extends AsyncTask<Void, Void, PostModel[]> {
private final String endCursor;
private final String id;
private final FetchListener<PostModel[]> fetchListener;
private String username;
public PostsFetcher(final String id, final FetchListener<PostModel[]> fetchListener) {
this.id = id;
this.endCursor = "";
this.fetchListener = fetchListener;
}
public PostsFetcher(final String id, final String endCursor, final FetchListener<PostModel[]> fetchListener) {
this.id = id;
this.endCursor = endCursor == null ? "" : endCursor;
this.fetchListener = fetchListener;
}
public PostsFetcher setUsername(final String username) {
this.username = username;
return this;
}
@Override
protected PostModel[] doInBackground(final Void... voids) {
final boolean isHashTag = id.charAt(0) == '#';
final String url;
if (isHashTag)
url = "https://www.instagram.com/graphql/query/?query_hash=ded47faa9a1aaded10161a2ff32abb6b&variables=" +
"{\"tag_name\":\"" + id.substring(1) + "\",\"first\":150,\"after\":\"" + endCursor + "\"}";
else
url = "https://www.instagram.com/graphql/query/?query_id=17880160963012870&id=" + id + "&first=50&after=" + endCursor;
PostModel[] result = null;
try {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
// to check if file exists
final File downloadDir = new File(Environment.getExternalStorageDirectory(), "Download");
File customDir = null;
if (Utils.settingsHelper.getBoolean(FOLDER_SAVE_TO)) {
final String customPath = Utils.settingsHelper.getString(FOLDER_PATH);
if (!Utils.isEmpty(customPath)) customDir = new File(customPath);
}
final JSONObject mediaPosts = new JSONObject(Utils.readFromConnection(conn)).getJSONObject("data")
.getJSONObject(isHashTag ? "hashtag" : Constants.EXTRAS_USER)
.getJSONObject(isHashTag ? "edge_hashtag_to_media" : "edge_owner_to_timeline_media");
final String endCursor;
final boolean hasNextPage;
final JSONObject pageInfo = mediaPosts.getJSONObject("page_info");
if (pageInfo.has("has_next_page")) {
hasNextPage = pageInfo.getBoolean("has_next_page");
endCursor = hasNextPage ? pageInfo.getString("end_cursor") : null;
} else {
hasNextPage = false;
endCursor = null;
}
final JSONArray edges = mediaPosts.getJSONArray("edges");
final PostModel[] models = new PostModel[edges.length()];
for (int i = 0; i < models.length; ++i) {
final JSONObject mediaNode = edges.getJSONObject(i).getJSONObject("node");
final JSONArray captions = mediaNode.getJSONObject("edge_media_to_caption").getJSONArray("edges");
final boolean isSlider = mediaNode.has("__typename") && mediaNode.getString("__typename").equals("GraphSidecar");
final boolean isVideo = mediaNode.getBoolean("is_video");
final MediaItemType itemType;
if (isSlider) itemType = MediaItemType.MEDIA_TYPE_SLIDER;
else if (isVideo) itemType = MediaItemType.MEDIA_TYPE_VIDEO;
else itemType = MediaItemType.MEDIA_TYPE_IMAGE;
models[i] = new PostModel(itemType, mediaNode.getString(Constants.EXTRAS_ID),
mediaNode.getString("display_url"), mediaNode.getString("thumbnail_src"),
mediaNode.getString(Constants.EXTRAS_SHORTCODE),
captions.length() > 0 ? captions.getJSONObject(0).getJSONObject("node").getString("text") : null,
mediaNode.getLong("taken_at_timestamp"));
Utils.checkExistence(downloadDir, customDir, username, isSlider, -1, models[i]);
}
if (models[models.length - 1] != null)
models[models.length - 1].setPageCursor(hasNextPage, endCursor);
result = models;
}
conn.disconnect();
} catch (Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_MAIN_POSTS_FETCHER, "doInBackground");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPostExecute(final PostModel[] postModels) {
if (fetchListener != null) fetchListener.onResult(postModels);
}
}

83
app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java

@ -0,0 +1,83 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Utils.logCollector;
public final class ProfileFetcher extends AsyncTask<Void, Void, ProfileModel> {
private final FetchListener<ProfileModel> fetchListener;
private final String userName;
public ProfileFetcher(String userName, FetchListener<ProfileModel> fetchListener) {
this.userName = userName;
this.fetchListener = fetchListener;
}
@Nullable
@Override
protected ProfileModel doInBackground(final Void... voids) {
ProfileModel result = null;
try {
final HttpURLConnection conn = (HttpURLConnection) new URL("https://www.instagram.com/" + userName + "/?__a=1").openConnection();
conn.setUseCaches(true);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
final JSONObject user = new JSONObject(Utils.readFromConnection(conn)).getJSONObject("graphql").getJSONObject(Constants.EXTRAS_USER);
boolean isPrivate = user.getBoolean("is_private");
final JSONObject timelineMedia = user.getJSONObject("edge_owner_to_timeline_media");
if (timelineMedia.has("edges")) {
final JSONArray edges = timelineMedia.getJSONArray("edges");
if (edges.length() > 0) isPrivate = false;
}
String url = user.optString("external_url");
if (Utils.isEmpty(url)) url = null;
result = new ProfileModel(isPrivate,
user.getBoolean("is_verified"),
user.getString(Constants.EXTRAS_ID),
userName,
user.getString("full_name"),
user.getString("biography"),
url,
user.getString("profile_pic_url"),
user.getString("profile_pic_url_hd"),
timelineMedia.getLong("count"),
user.getJSONObject("edge_followed_by").getLong("count"),
user.getJSONObject("edge_follow").getLong("count"));
}
conn.disconnect();
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_PROFILE_FETCHER, "doInBackground");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPostExecute(final ProfileModel result) {
if (fetchListener != null) fetchListener.onResult(result);
}
}

120
app/src/main/java/awais/instagrabber/asyncs/ProfilePictureFetcher.java

@ -0,0 +1,120 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import android.util.Pair;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import awais.instagrabber.models.enums.ProfilePictureFetchMode;
import static awais.instagrabber.utils.Utils.logCollector;
public final class ProfilePictureFetcher extends AsyncTask<Void, Void, String> {
private final FetchListener<String> fetchListener;
private final String userName, userId;
private final ProfilePictureFetchMode fetchMode;
public ProfilePictureFetcher(final String userName, final String userId, final FetchListener<String> fetchListener,
final ProfilePictureFetchMode fetchMode) {
this.fetchListener = fetchListener;
this.fetchMode = fetchMode;
this.userName = userName;
this.userId = userId;
}
@Override
protected String doInBackground(final Void... voids) {
String out = null;
try {
final String url;
if (fetchMode == ProfilePictureFetchMode.INSTADP)
url = "https://instadp.com/fullsize/" + userName;
else if (fetchMode == ProfilePictureFetchMode.INSTA_STALKER)
url = "https://insta-stalker.co/instadp_fullsize/?id=" + userName;
else // select from s1, s2, s3 but s1 works fine
url = "https://instafullsize.com/ifsapi/ig/photo/s1/" + userName + "?igid=" + userId;
// prolly http://167.99.85.4/instagram/userid?profile-url=the.badak
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setUseCaches(false);
if (fetchMode == ProfilePictureFetchMode.INSTAFULLSIZE) {
conn.setRequestMethod("GET");
conn.setRequestProperty("Authorization", "fjgt842ff582a");
}
final String result = conn.getResponseCode() == HttpURLConnection.HTTP_OK ? Utils.readFromConnection(conn) : null;
conn.disconnect();
if (!Utils.isEmpty(result)) {
final Document doc = Jsoup.parse(result);
boolean fallback = false;
if (fetchMode == ProfilePictureFetchMode.INSTADP) {
final int imgIndex = result.indexOf("preloadImg('"), lastIndex;
Element element = doc.selectFirst(".instadp");
if (element != null && (element = element.selectFirst(".picture")) != null)
out = element.attr("src");
else if ((element = doc.selectFirst(".download-btn")) != null)
out = element.attr("href");
else if (imgIndex != -1 && (lastIndex = result.indexOf("')", imgIndex)) != -1)
out = result.substring(imgIndex + 12, lastIndex);
else fallback = true;
} else if (fetchMode == ProfilePictureFetchMode.INSTAFULLSIZE) {
try {
final JSONObject object = new JSONObject(result);
out = object.getString("result");
} catch (final Exception e) {
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
fallback = true;
}
} else {
final Elements elements = doc.select("img[data-src]");
if (elements.size() > 0) out = elements.get(0).attr("data-src");
else fallback = true;
}
if (fallback) {
final Elements imgs = doc.getElementsByTag("img");
for (final Element img : imgs) {
final String imgStr = img.toString();
if (imgStr.contains("cdninstagram.com")) return img.attr("src");
}
}
}
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_PROFILE_PICTURE_FETCHER, "doInBackground",
new Pair<>("fetchMode", fetchMode));
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return out;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final String result) {
if (fetchListener != null) fetchListener.onResult(result);
}
}

102
app/src/main/java/awais/instagrabber/asyncs/StoryStatusFetcher.java

@ -0,0 +1,102 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Utils.logCollector;
public final class StoryStatusFetcher extends AsyncTask<Void, Void, StoryModel[]> {
private final String id;
private final FetchListener<StoryModel[]> fetchListener;
public StoryStatusFetcher(final String id, final FetchListener<StoryModel[]> fetchListener) {
this.id = id;
this.fetchListener = fetchListener;
}
@Override
protected StoryModel[] doInBackground(final Void... voids) {
StoryModel[] result = null;
final String url = "https://www.instagram.com/graphql/query/?query_hash=52a36e788a02a3c612742ed5146f1676&variables=" +
"{\"precomposed_overlay\":false,\"reel_ids\":[\"" + id + "\"]}";
try {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setInstanceFollowRedirects(false);
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
JSONObject data = new JSONObject(Utils.readFromConnection(conn)).getJSONObject("data");
JSONArray media;
if ((media = data.optJSONArray("reels_media")) != null && media.length() > 0 &&
(data = media.optJSONObject(0)) != null &&
(media = data.optJSONArray("items")) != null) {
final int mediaLen = media.length();
final StoryModel[] models = new StoryModel[mediaLen];
for (int i = 0; i < mediaLen; ++i) {
data = media.getJSONObject(i);
final boolean isVideo = data.getBoolean("is_video");
final JSONArray tappableObjects = data.optJSONArray("tappable_objects");
final int tappableLength = tappableObjects != null ? tappableObjects.length() : 0;
models[i] = new StoryModel(data.getString(Constants.EXTRAS_ID),
data.getString("display_url"),
isVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE,
data.optLong("taken_at_timestamp", 0));
final JSONArray videoResources = data.optJSONArray("video_resources");
if (isVideo && videoResources != null)
models[i].setVideoUrl(Utils.getHighQualityPost(videoResources, true));
for (int j = 0; j < tappableLength; ++j) {
JSONObject tappableObject = tappableObjects.getJSONObject(j);
if (tappableObject.optString("__typename").equals("GraphTappableFeedMedia")) {
tappableObject = tappableObject.getJSONObject("media");
models[i].setTappableShortCode(tappableObject.getString(Constants.EXTRAS_SHORTCODE));
break;
}
}
}
result = models;
}
}
conn.disconnect();
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_STORY_STATUS_FETCHER, "doInBackground");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final StoryModel[] result) {
if (fetchListener != null) fetchListener.onResult(result);
}
}

98
app/src/main/java/awais/instagrabber/asyncs/SuggestionsFetcher.java

@ -0,0 +1,98 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.SuggestionModel;
import awais.instagrabber.models.enums.SuggestionType;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.UrlEncoder;
import awais.instagrabber.utils.Utils;
public final class SuggestionsFetcher extends AsyncTask<String, String, SuggestionModel[]> {
private final FetchListener<SuggestionModel[]> fetchListener;
public SuggestionsFetcher(final FetchListener<SuggestionModel[]> fetchListener) {
this.fetchListener = fetchListener;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected SuggestionModel[] doInBackground(final String... params) {
SuggestionModel[] result = null;
try {
final HttpURLConnection conn = (HttpURLConnection) new URL("https://www.instagram.com/web/search/topsearch/?context=blended&count=50&query="
+ UrlEncoder.encodeUrl(params[0])).openConnection();
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
final String defaultHashTagPic = "https://www.instagram.com/static/images/hashtag/search-hashtag-default-avatar.png/1d8417c9a4f5.png";
final JSONObject jsonObject = new JSONObject(Utils.readFromConnection(conn));
conn.disconnect();
final JSONArray usersArray = jsonObject.getJSONArray("users");
final JSONArray hashtagsArray = jsonObject.getJSONArray("hashtags");
final int usersLen = usersArray.length();
final int hashtagsLen = hashtagsArray.length();
final ArrayList<SuggestionModel> suggestionModels = new ArrayList<>(usersLen + hashtagsLen);
for (int i = 0; i < hashtagsLen; i++) {
final JSONObject hashtagsArrayJSONObject = hashtagsArray.getJSONObject(i);
final JSONObject hashtag = hashtagsArrayJSONObject.getJSONObject("hashtag");
suggestionModels.add(new SuggestionModel(false,
hashtag.getString(Constants.EXTRAS_NAME),
null,
hashtag.optString("profile_pic_url", defaultHashTagPic),
SuggestionType.TYPE_HASHTAG,
hashtagsArrayJSONObject.optInt("position", suggestionModels.size() - 1)));
}
for (int i = 0; i < usersLen; i++) {
final JSONObject usersArrayJSONObject = usersArray.getJSONObject(i);
final JSONObject user = usersArrayJSONObject.getJSONObject(Constants.EXTRAS_USER);
suggestionModels.add(new SuggestionModel(user.getBoolean("is_verified"),
user.getString(Constants.EXTRAS_USERNAME),
user.getString("full_name"),
user.getString("profile_pic_url"),
SuggestionType.TYPE_USER,
usersArrayJSONObject.optInt("position", suggestionModels.size() - 1)));
}
suggestionModels.trimToSize();
Collections.sort(suggestionModels);
result = suggestionModels.toArray(new SuggestionModel[0]);
}
} catch (final Exception e) {
if (BuildConfig.DEBUG && !(e instanceof InterruptedIOException)) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPostExecute(final SuggestionModel[] result) {
if (fetchListener != null) fetchListener.onResult(result);
}
}

54
app/src/main/java/awais/instagrabber/asyncs/UsernameFetcher.java

@ -0,0 +1,54 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import androidx.annotation.Nullable;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
public final class UsernameFetcher extends AsyncTask<Void, Void, String> {
private final FetchListener<String> fetchListener;
private final String uid;
public UsernameFetcher(final String uid, final FetchListener<String> fetchListener) {
this.uid = uid;
this.fetchListener = fetchListener;
}
@Nullable
@Override
protected String doInBackground(final Void... voids) {
String result = null;
try {
final HttpURLConnection conn = (HttpURLConnection) new URL("https://i.instagram.com/api/v1/users/" + uid + "/info/").openConnection();
conn.setRequestProperty("User-Agent", Constants.USER_AGENT);
conn.setUseCaches(true);
final JSONObject user;
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK &&
(user = new JSONObject(Utils.readFromConnection(conn)).optJSONObject(Constants.EXTRAS_USER)) != null)
result = user.getString(Constants.EXTRAS_USERNAME);
conn.disconnect();
} catch (final Exception e) {
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPostExecute(final String result) {
if (fetchListener != null) fetchListener.onResult(result);
}
}

100
app/src/main/java/awais/instagrabber/asyncs/direct_messages/InboxFetcher.java

@ -0,0 +1,100 @@
package awais.instagrabber.asyncs.direct_messages;
import android.os.AsyncTask;
import android.util.Log;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.direct_messages.InboxModel;
import awais.instagrabber.models.direct_messages.InboxThreadModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.LocaleUtils;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.logCollector;
import static awaisomereport.LogCollector.LogFile;
public final class InboxFetcher extends AsyncTask<Void, Void, InboxModel> {
private final String endCursor;
private final FetchListener<InboxModel> fetchListener;
public InboxFetcher(final String endCursor, final FetchListener<InboxModel> fetchListener) {
this.endCursor = Utils.isEmpty(endCursor) ? "" : "?cursor=" + endCursor;
this.fetchListener = fetchListener;
}
@Nullable
@Override
protected InboxModel doInBackground(final Void... voids) {
InboxModel result = null;
final String url = "https://i.instagram.com/api/v1/direct_v2/inbox/" + endCursor;
try {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestProperty("User-Agent", Constants.USER_AGENT);
conn.setRequestProperty("Accept-Language", LocaleUtils.getCurrentLocale().getLanguage() + ",en-US;q=0.8");
conn.setUseCaches(false);
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
JSONObject data = new JSONObject(Utils.readFromConnection(conn));
// try (FileWriter fileWriter = new FileWriter(new File("/sdcard/test.json"))) {
// fileWriter.write(data.toString(2));
// }
final long seqId = data.optLong("seq_id");
final int pendingRequestsCount = data.optInt("pending_requests_total");
final boolean hasPendingTopRequests = data.optBoolean("has_pending_top_requests");
data = data.getJSONObject("inbox");
final boolean blendedInboxEnabled = data.optBoolean("blended_inbox_enabled");
final boolean hasOlder = data.optBoolean("has_older");
final int unseenCount = data.optInt("unseen_count");
final long unseenCountTimestamp = data.optLong("unseen_count_ts");
final String oldestCursor = data.optString("oldest_cursor");
InboxThreadModel[] inboxThreadModels = null;
final JSONArray threadsArray = data.optJSONArray("threads");
if (threadsArray != null) {
final int threadsLen = threadsArray.length();
inboxThreadModels = new InboxThreadModel[threadsLen];
for (int i = 0; i < threadsLen; ++i)
inboxThreadModels[i] = Utils.createInboxThreadModel(threadsArray.getJSONObject(i), false);
}
result = new InboxModel(hasOlder, hasPendingTopRequests,
blendedInboxEnabled, unseenCount, pendingRequestsCount,
seqId, unseenCountTimestamp, oldestCursor, inboxThreadModels);
}
conn.disconnect();
} catch (final Exception e) {
result = null;
if (logCollector != null)
logCollector.appendException(e, LogFile.ASYNC_DMS, "doInBackground");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final InboxModel inboxModel) {
if (fetchListener != null) fetchListener.onResult(inboxModel);
}
}

76
app/src/main/java/awais/instagrabber/asyncs/direct_messages/UserInboxFetcher.java

@ -0,0 +1,76 @@
package awais.instagrabber.asyncs.direct_messages;
import android.os.AsyncTask;
import android.util.Log;
import androidx.annotation.Nullable;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.direct_messages.InboxThreadModel;
import awais.instagrabber.models.enums.UserInboxDirection;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.logCollector;
import static awaisomereport.LogCollector.LogFile;
public final class UserInboxFetcher extends AsyncTask<Void, Void, InboxThreadModel> {
private final String id;
private final String endCursor;
private final FetchListener<InboxThreadModel> fetchListener;
private final String direction;
public UserInboxFetcher(final String id, final UserInboxDirection direction, final String endCursor,
final FetchListener<InboxThreadModel> fetchListener) {
this.id = id;
this.direction = "&direction=" + (direction == UserInboxDirection.NEWER ? "newer" : "older");
this.endCursor = !Utils.isEmpty(endCursor) ? "&cursor=" + endCursor : "";
this.fetchListener = fetchListener;
}
@Nullable
@Override
protected InboxThreadModel doInBackground(final Void... voids) {
InboxThreadModel result = null;
final String url = "https://i.instagram.com/api/v1/direct_v2/threads/" + id + "/?visual_message_return_type=unseen"
+ direction + endCursor;
// todo probably
// & seq_id = seqId
try {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestProperty("User-Agent", Constants.USER_AGENT);
conn.setUseCaches(false);
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
final JSONObject data = new JSONObject(Utils.readFromConnection(conn)).getJSONObject("thread");
result = Utils.createInboxThreadModel(data, true);
}
conn.disconnect();
} catch (final Exception e) {
result = null;
if (logCollector != null)
logCollector.appendException(e, LogFile.ASYNC_DMS_THREAD, "doInBackground");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final InboxThreadModel inboxThreadModel) {
if (fetchListener != null) fetchListener.onResult(inboxThreadModel);
}
}

105
app/src/main/java/awais/instagrabber/customviews/CircularImageView.java

@ -0,0 +1,105 @@
package awais.instagrabber.customviews;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import androidx.appcompat.widget.AppCompatImageView;
public final class CircularImageView extends AppCompatImageView {
private final int borderSize = 8;
private int color = Color.TRANSPARENT;
private final Paint paint = new Paint();
private final Paint paintBorder = new Paint();
private BitmapShader shader;
private Bitmap bitmap;
public CircularImageView(final Context context) {
super(context);
setup();
}
public CircularImageView(final Context context, final AttributeSet attrs) {
super(context, attrs);
setup();
}
public CircularImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
setup();
}
private void setup() {
paint.setAntiAlias(true);
paintBorder.setColor(color);
paintBorder.setAntiAlias(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setOutlineProvider(new ViewOutlineProvider() {
private int viewHeight;
private int viewWidth;
@Override
public void getOutline(final View view, final Outline outline) {
if (viewHeight == 0) viewHeight = getHeight();
if (viewWidth == 0) viewWidth = getWidth();
outline.setRoundRect(borderSize, borderSize, viewWidth - borderSize, viewHeight - borderSize, viewHeight >> 1);
}
});
}
}
@Override
public void onDraw(final Canvas canvas) {
final BitmapDrawable bitmapDrawable = (BitmapDrawable) getDrawable();
if (bitmapDrawable != null) {
final Bitmap prevBitmap = bitmap;
bitmap = bitmapDrawable.getBitmap();
final boolean changed = prevBitmap != bitmap;
if (bitmap != null) {
final int width = getWidth();
final int height = getHeight();
if (shader == null || changed) {
shader = new BitmapShader(Bitmap.createScaledBitmap(bitmap, width, height, true), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint.setShader(shader);
}
if (changed) color = 0;
paintBorder.setColor(color);
final int circleCenter = (width - borderSize) / 2;
final int position = circleCenter + (borderSize / 2);
canvas.drawCircle(position, position, position - 4.0f, paintBorder);
canvas.drawCircle(position, position, circleCenter - 4.0f, paint);
}
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setLayerType(LAYER_TYPE_HARDWARE, null);
}
@Override
protected void onDetachedFromWindow() {
setLayerType(LAYER_TYPE_NONE, null);
super.onDetachedFromWindow();
}
public void setStoriesBorder() {
this.color = Color.GREEN;
invalidate();
}
}

17
app/src/main/java/awais/instagrabber/customviews/CommentMentionClickSpan.java

@ -0,0 +1,17 @@
package awais.instagrabber.customviews;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;
import androidx.annotation.NonNull;
public final class CommentMentionClickSpan extends ClickableSpan {
@Override
public void onClick(@NonNull final View widget) { }
@Override
public void updateDrawState(@NonNull final TextPaint ds) {
ds.setColor(ds.linkColor);
}
}

25
app/src/main/java/awais/instagrabber/customviews/FixedImageView.java

@ -0,0 +1,25 @@
package awais.instagrabber.customviews;
import android.content.Context;
import android.util.AttributeSet;
import androidx.appcompat.widget.AppCompatImageView;
public final class FixedImageView extends AppCompatImageView {
public FixedImageView(final Context context) {
super(context);
}
public FixedImageView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public FixedImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(final int wMeasure, final int hMeasure) {
super.onMeasure(wMeasure, wMeasure);
}
}

986
app/src/main/java/awais/instagrabber/customviews/MouseDrawer.java

@ -0,0 +1,986 @@
package awais.instagrabber.customviews;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.GravityCompat;
import androidx.core.view.ViewCompat;
import androidx.customview.view.AbsSavedState;
import androidx.customview.widget.ViewDragHelper;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import awais.instagrabber.BuildConfig;
// exactly same as the LayoutDrawer with some edits
@SuppressLint("RtlHardcoded")
public class MouseDrawer extends ViewGroup {
@IntDef({ViewDragHelper.STATE_IDLE, ViewDragHelper.STATE_DRAGGING, ViewDragHelper.STATE_SETTLING})
@Retention(RetentionPolicy.SOURCE)
private @interface State {}
@IntDef(value = {Gravity.NO_GRAVITY, Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END}, flag = true)
@Retention(RetentionPolicy.SOURCE)
public @interface EdgeGravity {}
////////////////////////////////////////////////////////////////////////////////////
private static final boolean CHILDREN_DISALLOW_INTERCEPT = true;
////////////////////////////////////////////////////////////////////////////////////
private final ArrayList<View> mNonDrawerViews = new ArrayList<>();
private final ViewDragHelper mLeftDragger, mRightDragger;
private boolean mInLayout, mFirstLayout = true;
private float mDrawerElevation, mInitialMotionX, mInitialMotionY;
private int mDrawerState;
private List<DrawerListener> mListeners;
private Matrix mChildInvertedMatrix;
private Rect mChildHitRect;
public interface DrawerListener {
void onDrawerSlide(final View drawerView, @EdgeGravity final int gravity, final float slideOffset);
default void onDrawerOpened(final View drawerView, @EdgeGravity final int gravity) {}
default void onDrawerClosed(final View drawerView, @EdgeGravity final int gravity) {}
default void onDrawerStateChanged() {}
}
public MouseDrawer(@NonNull final Context context) {
this(context, null);
}
public MouseDrawer(@NonNull final Context context, @Nullable final AttributeSet attrs) {
this(context, attrs, 0);
}
public MouseDrawer(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
final float density = getResources().getDisplayMetrics().density;
this.mDrawerElevation = 10 * density;
final float touchSlopSensitivity = 0.5f; // was 1.0f
final float minFlingVelocity = 400 /* dips per second */ * density;
final ViewDragCallback mLeftCallback = new ViewDragCallback(Gravity.LEFT);
this.mLeftDragger = ViewDragHelper.create(this, touchSlopSensitivity, mLeftCallback);
this.mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
this.mLeftDragger.setMinVelocity(minFlingVelocity);
final ViewDragCallback mRightCallback = new ViewDragCallback(Gravity.RIGHT);
this.mRightDragger = ViewDragHelper.create(this, touchSlopSensitivity, mRightCallback);
this.mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
this.mRightDragger.setMinVelocity(minFlingVelocity);
try {
final Field edgeSizeField = ViewDragHelper.class.getDeclaredField("mEdgeSize");
if (!edgeSizeField.isAccessible()) edgeSizeField.setAccessible(true);
final int widthPixels = getResources().getDisplayMetrics().widthPixels; // whole screen
edgeSizeField.set(this.mLeftDragger, widthPixels / 2);
edgeSizeField.set(this.mRightDragger, widthPixels / 2);
} catch (final Exception e) {
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
}
mLeftCallback.setDragger(mLeftDragger);
mRightCallback.setDragger(mRightDragger);
setFocusableInTouchMode(true);
//setMotionEventSplittingEnabled(false);
}
public void setDrawerElevation(final float elevation) {
mDrawerElevation = elevation;
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
if (isDrawerView(child)) ViewCompat.setElevation(child, mDrawerElevation);
}
}
public float getDrawerElevation() {
return Build.VERSION.SDK_INT >= 21 ? mDrawerElevation : 0f;
}
public void addDrawerListener(@NonNull final DrawerListener listener) {
if (mListeners == null) mListeners = new ArrayList<>();
mListeners.add(listener);
}
private boolean isInBoundsOfChild(final float x, final float y, final View child) {
if (mChildHitRect == null) mChildHitRect = new Rect();
child.getHitRect(mChildHitRect);
return mChildHitRect.contains((int) x, (int) y);
}
private boolean dispatchTransformedGenericPointerEvent(final MotionEvent event, @NonNull final View child) {
final boolean handled;
final Matrix childMatrix = child.getMatrix();
if (!childMatrix.isIdentity()) {
final MotionEvent transformedEvent = getTransformedMotionEvent(event, child);
handled = child.dispatchGenericMotionEvent(transformedEvent);
transformedEvent.recycle();
} else {
final float offsetX = getScrollX() - child.getLeft();
final float offsetY = getScrollY() - child.getTop();
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchGenericMotionEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
@NonNull
private MotionEvent getTransformedMotionEvent(final MotionEvent event, @NonNull final View child) {
final float offsetX = getScrollX() - child.getLeft();
final float offsetY = getScrollY() - child.getTop();
final MotionEvent transformedEvent = MotionEvent.obtain(event);
transformedEvent.offsetLocation(offsetX, offsetY);
final Matrix childMatrix = child.getMatrix();
if (!childMatrix.isIdentity()) {
if (mChildInvertedMatrix == null) mChildInvertedMatrix = new Matrix();
childMatrix.invert(mChildInvertedMatrix);
transformedEvent.transform(mChildInvertedMatrix);
}
return transformedEvent;
}
void updateDrawerState(@State final int activeState, final View activeDrawer) {
final int leftState = mLeftDragger.getViewDragState();
final int rightState = mRightDragger.getViewDragState();
final int state;
if (leftState == ViewDragHelper.STATE_DRAGGING || rightState == ViewDragHelper.STATE_DRAGGING)
state = ViewDragHelper.STATE_DRAGGING;
else if (leftState == ViewDragHelper.STATE_SETTLING || rightState == ViewDragHelper.STATE_SETTLING)
state = ViewDragHelper.STATE_SETTLING;
else state = ViewDragHelper.STATE_IDLE;
if (activeDrawer != null && activeState == ViewDragHelper.STATE_IDLE) {
final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams();
if (lp.onScreen == 0) dispatchOnDrawerClosed(activeDrawer);
else if (lp.onScreen == 1) dispatchOnDrawerOpened(activeDrawer);
}
if (state != mDrawerState) {
mDrawerState = state;
if (mListeners != null) {
final int listenerCount = mListeners.size();
for (int i = listenerCount - 1; i >= 0; i--) mListeners.get(i).onDrawerStateChanged();
}
}
}
void dispatchOnDrawerClosed(@NonNull final View drawerView) {
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
if ((lp.openState & LayoutParams.FLAG_IS_OPENED) == 1) {
lp.openState = 0;
if (mListeners != null) {
final int listenerCount = mListeners.size();
for (int i = listenerCount - 1; i >= 0; i--) mListeners.get(i).onDrawerClosed(drawerView, lp.gravity);
}
}
}
void dispatchOnDrawerOpened(@NonNull final View drawerView) {
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
if ((lp.openState & LayoutParams.FLAG_IS_OPENED) == 0) {
lp.openState = LayoutParams.FLAG_IS_OPENED;
if (mListeners != null) {
final int listenerCount = mListeners.size();
for (int i = listenerCount - 1; i >= 0; i--) mListeners.get(i).onDrawerOpened(drawerView, lp.gravity);
}
}
}
void setDrawerViewOffset(@NonNull final View drawerView, final float slideOffset) {
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
if (slideOffset != lp.onScreen) {
lp.onScreen = slideOffset;
if (mListeners != null) {
final int listenerCount = mListeners.size();
for (int i = listenerCount - 1; i >= 0; i--)
mListeners.get(i).onDrawerSlide(drawerView, lp.gravity, slideOffset);
}
}
}
float getDrawerViewOffset(@NonNull final View drawerView) {
return ((LayoutParams) drawerView.getLayoutParams()).onScreen;
}
int getDrawerViewAbsoluteGravity(@NonNull final View drawerView) {
final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity;
return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this));
}
boolean checkDrawerViewAbsoluteGravity(final View drawerView, final int checkFor) {
final int absGravity = getDrawerViewAbsoluteGravity(drawerView);
return (absGravity & checkFor) == checkFor;
}
void moveDrawerToOffset(final View drawerView, final float slideOffset) {
final float oldOffset = getDrawerViewOffset(drawerView);
final int width = drawerView.getWidth();
final int oldPos = (int) (width * oldOffset);
final int newPos = (int) (width * slideOffset);
final int dx = newPos - oldPos;
drawerView.offsetLeftAndRight(checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT) ? dx : -dx);
setDrawerViewOffset(drawerView, slideOffset);
}
public View findOpenDrawer() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams childLp = (LayoutParams) child.getLayoutParams();
if ((childLp.openState & LayoutParams.FLAG_IS_OPENED) == 1) return child;
}
return null;
}
public View findDrawerWithGravity(final int gravity) {
final int absHorizGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)) & Gravity.HORIZONTAL_GRAVITY_MASK;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final int childAbsGravity = getDrawerViewAbsoluteGravity(child);
if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizGravity) return child;
}
return null;
}
@NonNull
static String gravityToString(@EdgeGravity final int gravity) {
if ((gravity & Gravity.LEFT) == Gravity.LEFT) return "LEFT";
if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) return "RIGHT";
return Integer.toHexString(gravity);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mFirstLayout = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
@SuppressLint("WrongConstant")
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
boolean hasDrawerOnLeftEdge = false;
boolean hasDrawerOnRightEdge = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (isContentView(child)) {
// Content views get measured at exactly the layout's size.
final int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
final int contentHeightSpec = MeasureSpec.makeMeasureSpec(heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY);
child.measure(contentWidthSpec, contentHeightSpec);
} else if (isDrawerView(child)) {
if (Build.VERSION.SDK_INT >= 21 && ViewCompat.getElevation(child) != mDrawerElevation)
ViewCompat.setElevation(child, mDrawerElevation);
final int childGravity = getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK;
final boolean isLeftEdgeDrawer = (childGravity == Gravity.LEFT);
if (isLeftEdgeDrawer && hasDrawerOnLeftEdge || !isLeftEdgeDrawer && hasDrawerOnRightEdge)
throw new IllegalStateException("Child drawer has absolute gravity " + gravityToString(childGravity)
+ " but this MouseDrawer already has a drawer view along that edge");
if (isLeftEdgeDrawer) hasDrawerOnLeftEdge = true;
else hasDrawerOnRightEdge = true;
final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, lp.leftMargin + lp.rightMargin, lp.width);
final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, lp.topMargin + lp.bottomMargin, lp.height);
child.measure(drawerWidthSpec, drawerHeightSpec);
} else
throw new IllegalStateException("Child " + child + " at index " + i
+ " does not have a valid layout_gravity - must be Gravity.LEFT, Gravity.RIGHT or Gravity.NO_GRAVITY");
}
}
}
@Override
protected void onLayout(final boolean changed, final int left, final int top, final int right, final int bottom) {
mInLayout = true;
final int width = right - left;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (isContentView(child)) {
child.layout(lp.leftMargin, lp.topMargin, lp.leftMargin + child.getMeasuredWidth(),
lp.topMargin + child.getMeasuredHeight());
} else { // Drawer, if it wasn't onMeasure would have thrown an exception.
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final int childLeft;
final float newOffset;
if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) {
childLeft = -childWidth + (int) (childWidth * lp.onScreen);
newOffset = (float) (childWidth + childLeft) / childWidth;
} else { // Right; onMeasure checked for us.
childLeft = width - (int) (childWidth * lp.onScreen);
newOffset = (float) (width - childLeft) / childWidth;
}
final boolean changeOffset = newOffset != lp.onScreen;
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (vgrav) {
default:
case Gravity.TOP:
child.layout(childLeft, lp.topMargin, childLeft + childWidth, lp.topMargin + childHeight);
break;
case Gravity.BOTTOM: {
final int height = bottom - top;
child.layout(childLeft, height - lp.bottomMargin - child.getMeasuredHeight(),
childLeft + childWidth, height - lp.bottomMargin);
break;
}
case Gravity.CENTER_VERTICAL: {
final int height = bottom - top;
int childTop = (height - childHeight) / 2;
if (childTop < lp.topMargin) childTop = lp.topMargin;
else if (childTop + childHeight > height - lp.bottomMargin)
childTop = height - lp.bottomMargin - childHeight;
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
break;
}
}
if (changeOffset) setDrawerViewOffset(child, newOffset);
final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE;
if (child.getVisibility() != newVisibility) child.setVisibility(newVisibility);
}
}
}
mInLayout = false;
mFirstLayout = false;
}
@Override
public void requestLayout() {
if (!mInLayout) super.requestLayout();
}
@Override
public void computeScroll() {
final boolean leftDraggerSettling = mLeftDragger.continueSettling(true);
final boolean rightDraggerSettling = mRightDragger.continueSettling(true);
if (leftDraggerSettling || rightDraggerSettling) postInvalidateOnAnimation();
}
private static boolean hasOpaqueBackground(@NonNull final View v) {
final Drawable bg = v.getBackground();
if (bg != null) return bg.getOpacity() == PixelFormat.OPAQUE;
return false;
}
@Override
protected boolean drawChild(@NonNull final Canvas canvas, final View child, final long drawingTime) {
final int height = getHeight();
final boolean drawingContent = isContentView(child);
int clipLeft = 0, clipRight = getWidth();
final int restoreCount = canvas.save();
if (drawingContent) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View v = getChildAt(i);
if (v != child && v.getVisibility() == VISIBLE && hasOpaqueBackground(v) && isDrawerView(v) && v.getHeight() >= height) {
if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) {
final int vright = v.getRight();
if (vright > clipLeft) clipLeft = vright;
} else {
final int vleft = v.getLeft();
if (vleft < clipRight) clipRight = vleft;
}
}
}
canvas.clipRect(clipLeft, 0, clipRight, getHeight());
}
final boolean result = super.drawChild(canvas, child, drawingTime);
canvas.restoreToCount(restoreCount);
return result;
}
boolean isContentView(@NonNull final View child) {
return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY;
}
boolean isDrawerView(@NonNull final View child) {
final int gravity = ((LayoutParams) child.getLayoutParams()).gravity;
final int absGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(child));
return (absGravity & Gravity.LEFT) != 0 || (absGravity & Gravity.RIGHT) != 0;
}
@Override
public boolean onInterceptTouchEvent(@NonNull final MotionEvent ev) {
final int action = ev.getActionMasked();
// "|" used deliberately here; both methods should be invoked.
final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | mRightDragger.shouldInterceptTouchEvent(ev);
switch (action) {
case MotionEvent.ACTION_DOWN:
mInitialMotionX = ev.getX();
mInitialMotionY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
closeDrawers(true);
}
return interceptForDrag || hasPeekingDrawer();
}
@Override
public boolean dispatchGenericMotionEvent(@NonNull final MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0 || event.getAction() == MotionEvent.ACTION_HOVER_EXIT)
return super.dispatchGenericMotionEvent(event);
final int childrenCount = getChildCount();
if (childrenCount != 0) {
final float x = event.getX();
final float y = event.getY();
// Walk through children from top to bottom.
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (isInBoundsOfChild(x, y, child) && !isContentView(child) && dispatchTransformedGenericPointerEvent(event, child))
return true;
}
}
return false;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(final MotionEvent ev) {
mLeftDragger.processTouchEvent(ev);
mRightDragger.processTouchEvent(ev);
final int action = ev.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
mInitialMotionX = ev.getX();
mInitialMotionY = ev.getY();
break;
case MotionEvent.ACTION_UP:
final float x = ev.getX();
final float y = ev.getY();
boolean peekingOnly = true;
final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y);
if (touchedView != null && isContentView(touchedView)) {
final float dx = x - mInitialMotionX;
final float dy = y - mInitialMotionY;
final int slop = mLeftDragger.getTouchSlop();
if (dx * dx + dy * dy < slop * slop) {
// Taps close a dimmed open drawer but only if it isn't locked open.
final View openDrawer = findOpenDrawer();
if (openDrawer != null) peekingOnly = false;
}
}
closeDrawers(peekingOnly);
break;
case MotionEvent.ACTION_CANCEL:
closeDrawers(true);
break;
}
return true;
}
@Override
public void requestDisallowInterceptTouchEvent(final boolean disallowIntercept) {
if (CHILDREN_DISALLOW_INTERCEPT || (!mLeftDragger.isEdgeTouched(ViewDragHelper.EDGE_LEFT) && !mRightDragger.isEdgeTouched(ViewDragHelper.EDGE_RIGHT)))
super.requestDisallowInterceptTouchEvent(disallowIntercept);
if (disallowIntercept) closeDrawers(true);
}
public void closeDrawers() {
closeDrawers(false);
}
void closeDrawers(final boolean peekingOnly) {
boolean needsInvalidate = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (isDrawerView(child) && (!peekingOnly || lp.isPeeking)) {
final int childWidth = child.getWidth();
if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT))
needsInvalidate |= mLeftDragger.smoothSlideViewTo(child, -childWidth, child.getTop());
else
needsInvalidate |= mRightDragger.smoothSlideViewTo(child, getWidth(), child.getTop());
lp.isPeeking = false;
}
}
if (needsInvalidate) invalidate();
}
public void openDrawer(@NonNull final View drawerView, final boolean animate) {
if (isDrawerView(drawerView)) {
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
if (mFirstLayout) {
lp.onScreen = 1.f;
lp.openState = LayoutParams.FLAG_IS_OPENED;
} else if (animate) {
lp.openState |= LayoutParams.FLAG_IS_OPENING;
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT))
mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop());
else
mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), drawerView.getTop());
} else {
moveDrawerToOffset(drawerView, 1.f);
updateDrawerState(ViewDragHelper.STATE_IDLE, drawerView);
drawerView.setVisibility(VISIBLE);
}
invalidate();
return;
}
throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
}
public void openDrawer(@NonNull final View drawerView) {
openDrawer(drawerView, true);
}
// public void openDrawer(@EdgeGravity final int gravity, final boolean animate) {
// final View drawerView = findDrawerWithGravity(gravity);
// if (drawerView != null) openDrawer(drawerView, animate);
// else throw new IllegalArgumentException("No drawer view found with gravity " + gravityToString(gravity));
// }
// public void openDrawer(@EdgeGravity final int gravity) {
// openDrawer(gravity, true);
// }
public void closeDrawer(@NonNull final View drawerView) {
closeDrawer(drawerView, true);
}
public void closeDrawer(@NonNull final View drawerView, final boolean animate) {
if (isDrawerView(drawerView)) {
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
if (mFirstLayout) {
lp.onScreen = 0.f;
lp.openState = 0;
} else if (animate) {
lp.openState |= LayoutParams.FLAG_IS_CLOSING;
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT))
mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), drawerView.getTop());
else
mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop());
} else {
moveDrawerToOffset(drawerView, 0.f);
updateDrawerState(ViewDragHelper.STATE_IDLE, drawerView);
drawerView.setVisibility(INVISIBLE);
}
invalidate();
} else throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
}
// public void closeDrawer(@EdgeGravity final int gravity) {
// closeDrawer(gravity, true);
// }
// public void closeDrawer(@EdgeGravity final int gravity, final boolean animate) {
// final View drawerView = findDrawerWithGravity(gravity);
// if (drawerView != null) closeDrawer(drawerView, animate);
// else throw new IllegalArgumentException("No drawer view found with gravity " + gravityToString(gravity));
// }
public boolean isDrawerOpen(@NonNull final View drawer) {
if (isDrawerView(drawer)) return (((LayoutParams) drawer.getLayoutParams()).openState & LayoutParams.FLAG_IS_OPENED) == 1;
else throw new IllegalArgumentException("View " + drawer + " is not a drawer");
}
// public boolean isDrawerOpen(@EdgeGravity final int drawerGravity) {
// final View drawerView = findDrawerWithGravity(drawerGravity);
// return drawerView != null && isDrawerOpen(drawerView);
// }
public boolean isDrawerVisible(@NonNull final View drawer) {
if (isDrawerView(drawer)) return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0;
throw new IllegalArgumentException("View " + drawer + " is not a drawer");
}
// public boolean isDrawerVisible(@EdgeGravity final int drawerGravity) {
// final View drawerView = findDrawerWithGravity(drawerGravity);
// return drawerView != null && isDrawerVisible(drawerView);
// }
private boolean hasPeekingDrawer() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.isPeeking) return true;
}
return false;
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams params) {
return params instanceof LayoutParams ? new LayoutParams((LayoutParams) params) :
params instanceof ViewGroup.MarginLayoutParams ? new LayoutParams((MarginLayoutParams) params) : new LayoutParams(params);
}
@Override
protected boolean checkLayoutParams(final ViewGroup.LayoutParams params) {
return params instanceof LayoutParams && super.checkLayoutParams(params);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
public void addFocusables(final ArrayList<View> views, final int direction, final int focusableMode) {
if (getDescendantFocusability() != FOCUS_BLOCK_DESCENDANTS) {
final int childCount = getChildCount();
boolean isDrawerOpen = false;
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (!isDrawerView(child)) mNonDrawerViews.add(child);
else if (isDrawerOpen(child)) {
isDrawerOpen = true;
child.addFocusables(views, direction, focusableMode);
}
}
if (!isDrawerOpen) {
final int nonDrawerViewsCount = mNonDrawerViews.size();
for (int i = 0; i < nonDrawerViewsCount; ++i) {
final View child = mNonDrawerViews.get(i);
if (child.getVisibility() == View.VISIBLE) child.addFocusables(views, direction, focusableMode);
}
}
mNonDrawerViews.clear();
}
}
private boolean hasVisibleDrawer() {
return findVisibleDrawer() != null;
}
@Nullable
final View findVisibleDrawer() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (isDrawerView(child) && isDrawerVisible(child)) return child;
}
return null;
}
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) {
event.startTracking();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(final int keyCode, final KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
final View visibleDrawer = findVisibleDrawer();
if (visibleDrawer != null && isDrawerView(visibleDrawer)) closeDrawers();
return visibleDrawer != null;
}
return super.onKeyUp(keyCode, event);
}
@Override
protected void onRestoreInstanceState(final Parcelable state) {
if (state instanceof SavedState) {
final SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
if (ss.openDrawerGravity != Gravity.NO_GRAVITY) {
final View toOpen = findDrawerWithGravity(ss.openDrawerGravity);
if (toOpen != null) openDrawer(toOpen);
}
} else super.onRestoreInstanceState(state);
}
@Override
protected Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
assert superState != null;
final SavedState ss = new SavedState(superState);
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Is the current child fully opened (that is, not closing)?
final boolean isOpenedAndNotClosing = (lp.openState == LayoutParams.FLAG_IS_OPENED);
// Is the current child opening?
final boolean isClosedAndOpening = (lp.openState == LayoutParams.FLAG_IS_OPENING);
if (isOpenedAndNotClosing || isClosedAndOpening) {
// If one of the conditions above holds, save the child's gravity so that we open that child during state restore.
ss.openDrawerGravity = lp.gravity;
break;
}
}
return ss;
}
@Override
public void addView(final View child, final int index, final ViewGroup.LayoutParams params) {
super.addView(child, index, params);
final View openDrawer = findOpenDrawer();
if (openDrawer == null) isDrawerView(child);
}
protected static class SavedState extends AbsSavedState {
public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
@NonNull
@Override
public SavedState createFromParcel(final Parcel in, final ClassLoader loader) {
return new SavedState(in, loader);
}
@NonNull
@Override
public SavedState createFromParcel(final Parcel in) {
return new SavedState(in, null);
}
@NonNull
@Override
public SavedState[] newArray(final int size) {
return new SavedState[size];
}
};
int openDrawerGravity = Gravity.NO_GRAVITY;
public SavedState(@NonNull final Parcelable superState) {
super(superState);
}
public SavedState(@NonNull final Parcel in, @Nullable final ClassLoader loader) {
super(in, loader);
openDrawerGravity = in.readInt();
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(openDrawerGravity);
}
}
private class ViewDragCallback extends ViewDragHelper.Callback {
private final int mAbsGravity;
private ViewDragHelper mDragger;
ViewDragCallback(final int gravity) {
mAbsGravity = gravity;
}
public void setDragger(final ViewDragHelper dragger) {
mDragger = dragger;
}
@Override
public boolean tryCaptureView(@NonNull final View child, final int pointerId) {
return isDrawerView(child) && checkDrawerViewAbsoluteGravity(child, mAbsGravity);
}
@Override
public void onViewDragStateChanged(final int state) {
updateDrawerState(state, mDragger.getCapturedView());
}
@Override
public void onViewPositionChanged(@NonNull final View changedView, final int left, final int top, final int dx, final int dy) {
final float offset;
final int childWidth = changedView.getWidth();
if (checkDrawerViewAbsoluteGravity(changedView, Gravity.LEFT)) offset = (float) (childWidth + left) / childWidth;
else offset = (float) (getWidth() - left) / childWidth;
setDrawerViewOffset(changedView, offset);
changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE);
invalidate();
}
@Override
public void onViewCaptured(@NonNull final View capturedChild, final int activePointerId) {
final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams();
lp.isPeeking = false;
closeOtherDrawer();
}
private void closeOtherDrawer() {
final int otherGrav = mAbsGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT;
final View toClose = findDrawerWithGravity(otherGrav);
if (toClose != null) closeDrawer(toClose);
}
@Override
public void onViewReleased(@NonNull final View releasedChild, final float xvel, final float yvel) {
final float offset = getDrawerViewOffset(releasedChild);
final int childWidth = releasedChild.getWidth();
final int left;
if (checkDrawerViewAbsoluteGravity(releasedChild, Gravity.LEFT))
left = xvel > 0 || (xvel == 0 && offset > 0.5f) ? 0 : -childWidth;
else {
final int width = getWidth();
left = xvel < 0 || (xvel == 0 && offset > 0.5f) ? width - childWidth : width;
}
mDragger.settleCapturedViewAt(left, releasedChild.getTop());
invalidate();
}
@Override
public void onEdgeDragStarted(final int edgeFlags, final int pointerId) {
final View toCapture;
if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT)
toCapture = findDrawerWithGravity(Gravity.LEFT);
else toCapture = findDrawerWithGravity(Gravity.RIGHT);
if (toCapture != null && isDrawerView(toCapture)) mDragger.captureChildView(toCapture, pointerId);
}
@Override
public int getViewHorizontalDragRange(@NonNull final View child) {
return isDrawerView(child) ? child.getWidth() : 0;
}
@Override
public int clampViewPositionHorizontal(@NonNull final View child, final int left, final int dx) {
if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) return Math.max(-child.getWidth(), Math.min(left, 0));
final int width = getWidth();
return Math.max(width - child.getWidth(), Math.min(left, width));
}
@Override
public int clampViewPositionVertical(@NonNull final View child, final int top, final int dy) {
return child.getTop();
}
}
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
private static final int FLAG_IS_CLOSING = 0x4;
public static final int FLAG_IS_OPENED = 0x1;
public static final int FLAG_IS_OPENING = 0x2;
public int openState;
@EdgeGravity
public int gravity = Gravity.NO_GRAVITY;
public boolean isPeeking;
public float onScreen;
public LayoutParams(@NonNull final Context c, @Nullable final AttributeSet attrs) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs, new int[]{android.R.attr.layout_gravity});
try {
this.gravity = a.getInt(0, Gravity.NO_GRAVITY);
} finally {
a.recycle();
}
}
public LayoutParams(final int width, final int height) {
super(width, height);
}
public LayoutParams(@NonNull final LayoutParams source) {
super(source);
this.gravity = source.gravity;
}
public LayoutParams(@NonNull final ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(@NonNull final ViewGroup.MarginLayoutParams source) {
super(source);
}
}
}

179
app/src/main/java/awais/instagrabber/customviews/RamboTextView.java

@ -0,0 +1,179 @@
package awais.instagrabber.customviews;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.RectF;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import awais.instagrabber.R;
import awais.instagrabber.interfaces.MentionClickListener;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.utils.Utils;
public final class RamboTextView extends AppCompatTextView {
private static final int highlightBackgroundSpanKey = R.id.tvComment;
private static final RectF touchedLineBounds = new RectF();
private ClickableSpan clickableSpanUnderTouchOnActionDown;
private MentionClickListener mentionClickListener;
private boolean isUrlHighlighted, isExpandable, isExpanded;
public RamboTextView(final Context context) {
super(context);
}
public RamboTextView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public RamboTextView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setMentionClickListener(final MentionClickListener mentionClickListener) {
this.mentionClickListener = mentionClickListener;
}
public void setCaptionIsExpandable(final boolean isExpandable) {
this.isExpandable = isExpandable;
}
public void setCaptionIsExpanded(final boolean isExpanded) {
this.isExpanded = isExpanded;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(final MotionEvent event) {
final CharSequence text = getText();
if (text instanceof SpannableString || text instanceof SpannableStringBuilder) {
final Spannable spanText = (Spannable) text;
final ClickableSpan clickableSpanUnderTouch = findClickableSpanUnderTouch(this, spanText, event);
final int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) clickableSpanUnderTouchOnActionDown = clickableSpanUnderTouch;
final boolean touchStartedOverAClickableSpan = clickableSpanUnderTouchOnActionDown != null;
final boolean isURLSpan = clickableSpanUnderTouch instanceof URLSpan;
// feed view caption hacks
if (isExpandable && !touchStartedOverAClickableSpan)
return !isExpanded | super.onTouchEvent(event); // short operator, because we want two shits to work
final Object tag = getTag();
final FeedModel feedModel = tag instanceof FeedModel ? (FeedModel) tag : null;
switch (action) {
case MotionEvent.ACTION_DOWN:
if (feedModel != null) feedModel.setMentionClicked(false);
if (clickableSpanUnderTouch != null) highlightUrl(clickableSpanUnderTouch, spanText);
return isURLSpan ? super.onTouchEvent(event) : touchStartedOverAClickableSpan;
case MotionEvent.ACTION_UP:
if (touchStartedOverAClickableSpan && clickableSpanUnderTouch == clickableSpanUnderTouchOnActionDown) {
dispatchUrlClick(spanText, clickableSpanUnderTouch);
if (feedModel != null) feedModel.setMentionClicked(true);
}
cleanupOnTouchUp(spanText);
return isURLSpan ? super.onTouchEvent(event) : touchStartedOverAClickableSpan;
case MotionEvent.ACTION_MOVE:
if (feedModel != null) feedModel.setMentionClicked(false);
if (clickableSpanUnderTouch != null) highlightUrl(clickableSpanUnderTouch, spanText);
else removeUrlHighlightColor(spanText);
return isURLSpan ? super.onTouchEvent(event) : touchStartedOverAClickableSpan;
case MotionEvent.ACTION_CANCEL:
if (feedModel != null) feedModel.setMentionClicked(false);
cleanupOnTouchUp(spanText);
return super.onTouchEvent(event);
}
}
return super.onTouchEvent(event);
}
protected void dispatchUrlClick(final Spanned s, final ClickableSpan clickableSpan) {
if (mentionClickListener != null) {
final int spanStart = s.getSpanStart(clickableSpan);
final boolean ishHashtag = s.charAt(spanStart) == '#';
final int start = ishHashtag || s.charAt(spanStart) != '@' ? spanStart : spanStart + 1;
CharSequence subSequence = s.subSequence(start, s.getSpanEnd(clickableSpan));
// for feed ellipsize
final int indexOfEllipsize = Utils.indexOfChar(subSequence, '…', 0);
if (indexOfEllipsize != -1)
subSequence = subSequence.subSequence(0, indexOfEllipsize - 1);
mentionClickListener.onClick(this, subSequence.toString(), ishHashtag);
}
}
protected void highlightUrl(final ClickableSpan clickableSpan, final Spannable text) {
if (!isUrlHighlighted) {
isUrlHighlighted = true;
final int spanStart = text.getSpanStart(clickableSpan);
final int spanEnd = text.getSpanEnd(clickableSpan);
final BackgroundColorSpan highlightSpan = new BackgroundColorSpan(getHighlightColor());
text.setSpan(highlightSpan, spanStart, spanEnd, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
setTag(highlightBackgroundSpanKey, highlightSpan);
Selection.setSelection(text, spanStart, spanEnd);
}
}
protected void removeUrlHighlightColor(final Spannable text) {
if (isUrlHighlighted) {
isUrlHighlighted = false;
final BackgroundColorSpan highlightSpan = (BackgroundColorSpan) getTag(highlightBackgroundSpanKey);
text.removeSpan(highlightSpan);
Selection.removeSelection(text);
}
}
private void cleanupOnTouchUp(final Spannable text) {
clickableSpanUnderTouchOnActionDown = null;
removeUrlHighlightColor(text);
}
@Nullable
private static ClickableSpan findClickableSpanUnderTouch(@NonNull final TextView textView, final Spannable text, @NonNull final MotionEvent event) {
final int touchX = (int) (event.getX() - textView.getTotalPaddingLeft() + textView.getScrollX());
final int touchY = (int) (event.getY() - textView.getTotalPaddingTop() + textView.getScrollY());
final Layout layout = textView.getLayout();
final int touchedLine = layout.getLineForVertical(touchY);
final int touchOffset = layout.getOffsetForHorizontal(touchedLine, touchX);
touchedLineBounds.left = layout.getLineLeft(touchedLine);
touchedLineBounds.top = layout.getLineTop(touchedLine);
touchedLineBounds.right = layout.getLineWidth(touchedLine) + touchedLineBounds.left;
touchedLineBounds.bottom = layout.getLineBottom(touchedLine);
if (touchedLineBounds.contains(touchX, touchY)) {
final Object[] spans = text.getSpans(touchOffset, touchOffset, ClickableSpan.class);
for (final Object span : spans)
if (span instanceof ClickableSpan) return (ClickableSpan) span;
}
return null;
}
}

182
app/src/main/java/awais/instagrabber/customviews/RemixDrawerLayout.java

@ -0,0 +1,182 @@
package awais.instagrabber.customviews;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.GravityCompat;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import awais.instagrabber.R;
public final class RemixDrawerLayout extends MouseDrawer implements MouseDrawer.DrawerListener {
private final FrameLayout frameLayout;
private View drawerView;
private RecyclerView scroll, feedPosts;
private float startX;
public RemixDrawerLayout(@NonNull final Context context) {
this(context, null);
}
public RemixDrawerLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) {
this(context, attrs, 0);
}
public RemixDrawerLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
super.setDrawerElevation(getDrawerElevation());
addDrawerListener(this);
frameLayout = new FrameLayout(context);
frameLayout.setPadding(0, 0, 0, 0);
super.addView(frameLayout);
}
@Override
public void addView(@NonNull final View child, final ViewGroup.LayoutParams params) {
child.setLayoutParams(params);
addView(child);
}
@Override
public void addView(@NonNull final View child) {
if (child.getTag() != null) super.addView(child);
else frameLayout.addView(child);
}
@Override
public boolean onInterceptTouchEvent(@NonNull final MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
// another one of my own weird hack thingies to make this app work
if (feedPosts == null) feedPosts = findViewById(R.id.feedPosts);
if (feedPosts != null) {
for (int i = 0; i < feedPosts.getChildCount(); ++i) {
final View viewHolder = feedPosts.getChildAt(i);
final View mediaList = viewHolder.findViewById(R.id.media_list);
if (mediaList instanceof ViewPager) {
final ViewPager viewPager = (ViewPager) mediaList;
final Rect rect = new Rect();
viewPager.getGlobalVisibleRect(rect);
final boolean touchIsInMediaList = rect.contains((int) x, (int) y);
if (touchIsInMediaList) {
final PagerAdapter adapter = viewPager.getAdapter();
final int count = adapter != null ? adapter.getCount() : 0;
if (count < 1 || viewPager.getCurrentItem() != count - 1) return false;
break;
}
}
}
}
// thanks to Fede @ https://stackoverflow.com/questions/6920137/android-viewpager-and-horizontalscrollview/7258579#7258579
if (scroll == null) scroll = findViewById(R.id.highlightsList);
if (scroll != null) {
final boolean touchIsInRecycler = x >= scroll.getLeft() && x < scroll.getRight()
&& y >= scroll.getTop() && scroll.getBottom() > y;
if (touchIsInRecycler) {
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_CANCEL) return super.onInterceptTouchEvent(ev);
if (action == MotionEvent.ACTION_DOWN) startX = x;
else if (action == MotionEvent.ACTION_MOVE) {
final int scrollRange = scroll.computeHorizontalScrollRange();
final int scrollOffset = scroll.computeHorizontalScrollOffset();
final boolean scrollable = scrollRange > scroll.getWidth();
final boolean draggingFromRight = startX > x;
if (scrollOffset < 1) {
if (!scrollable) return super.onInterceptTouchEvent(ev);
else if (!draggingFromRight) return super.onInterceptTouchEvent(ev);
} else if (scrollable && draggingFromRight && scrollRange - scrollOffset == scroll.computeHorizontalScrollExtent()) {
return super.onInterceptTouchEvent(ev);
}
return false;
}
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public void onDrawerSlide(@NonNull final View view, @EdgeGravity final int gravity, final float slideOffset) {
drawerView = view;
final int absHorizGravity = getDrawerViewAbsoluteGravity(GravityCompat.START);
final int childAbsGravity = getDrawerViewAbsoluteGravity(drawerView);
final Window window = getActivity(getContext()).getWindow();
final boolean isRtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
|| window.getDecorView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
|| getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
final int drawerViewWidth = drawerView.getWidth();
// for (int i = 0; i < frameLayout.getChildCount(); i++) {
// final View child = frameLayout.getChildAt(i);
//
// final boolean isLeftDrawer = isRtl == (childAbsGravity != absHorizGravity);
// float width = isLeftDrawer ? drawerViewWidth : -drawerViewWidth;
//
// child.setX(width * slideOffset);
// }
final boolean isLeftDrawer = isRtl == (childAbsGravity != absHorizGravity);
float width = isLeftDrawer ? drawerViewWidth : -drawerViewWidth;
frameLayout.setX(width * (isRtl ? -slideOffset : slideOffset));
}
@Override
public void openDrawer(@NonNull final View drawerView, final boolean animate) {
super.openDrawer(drawerView, animate);
post(() -> onDrawerSlide(drawerView, Gravity.NO_GRAVITY, isDrawerOpen(drawerView) ? 1f : 0f));
}
@Override
protected void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (drawerView != null) onDrawerSlide(drawerView, Gravity.NO_GRAVITY, isDrawerOpen(drawerView) ? 1f : 0f);
}
private static Activity getActivity(final Context context) {
if (context != null) {
if (context instanceof Activity) return (Activity) context;
if (context instanceof ContextWrapper)
return getActivity(((ContextWrapper) context).getBaseContext());
}
return null;
}
final int getDrawerViewAbsoluteGravity(final int gravity) {
return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)) & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK;
}
final int getDrawerViewAbsoluteGravity(@NonNull final View drawerView) {
final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity;
return getDrawerViewAbsoluteGravity(gravity);
}
}

37
app/src/main/java/awais/instagrabber/customviews/helpers/GridAutofitLayoutManager.java

@ -0,0 +1,37 @@
package awais.instagrabber.customviews.helpers;
import android.content.Context;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.utils.Utils;
public class GridAutofitLayoutManager extends GridLayoutManager {
private int mColumnWidth;
private boolean mColumnWidthChanged = true;
public GridAutofitLayoutManager(Context context, int columnWidth) {
super(context, 1);
if (columnWidth <= 0) columnWidth = (int) (48 * Utils.displayMetrics.density);
if (columnWidth > 0 && columnWidth != mColumnWidth) {
mColumnWidth = columnWidth;
mColumnWidthChanged = true;
}
}
@Override
public void onLayoutChildren(final RecyclerView.Recycler recycler, final RecyclerView.State state) {
final int width = getWidth();
final int height = getHeight();
if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0) {
final int totalSpace = getOrientation() == VERTICAL ? width - getPaddingRight() - getPaddingLeft()
: height - getPaddingTop() - getPaddingBottom();
setSpanCount(Math.max(1, totalSpace / mColumnWidth));
mColumnWidthChanged = false;
}
super.onLayoutChildren(recycler, state);
}
}

31
app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java

@ -0,0 +1,31 @@
package awais.instagrabber.customviews.helpers;
import android.graphics.Rect;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private final int spacing;
public GridSpacingItemDecoration(int spacing) {
this.spacing = spacing;
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
final RecyclerView.LayoutManager manager = parent.getLayoutManager();
if (manager instanceof GridLayoutManager) {
final int spanCount = ((GridLayoutManager) manager).getSpanCount();
final int position = parent.getChildAdapterPosition(view);
final int column = position % spanCount;
outRect.left = column * spacing / spanCount;
outRect.right = spacing - (column + 1) * spacing / spanCount;
if (position < spanCount) outRect.top = spacing;
outRect.bottom = spacing;
}
}
}

67
app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoader.java

@ -0,0 +1,67 @@
package awais.instagrabber.customviews.helpers;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.interfaces.LazyLoadListener;
// thanks to nesquena's EndlessRecyclerViewScrollListener
// https://gist.github.com/nesquena/d09dc68ff07e845cc622
public final class RecyclerLazyLoader extends RecyclerView.OnScrollListener {
private int currentPage = 0; // The current offset index of data you have loaded
private int previousTotalItemCount = 0; // The total number of items in the dataset after the last load
private boolean loading = true; // True if we are still waiting for the last set of data to load.
private final int visibleThreshold; // The minimum amount of items to have below your current scroll position before loading more.
private final LazyLoadListener lazyLoadListener;
private final RecyclerView.LayoutManager layoutManager;
public RecyclerLazyLoader(@NonNull final RecyclerView.LayoutManager layoutManager, final LazyLoadListener lazyLoadListener) {
this.layoutManager = layoutManager;
this.lazyLoadListener = lazyLoadListener;
if (layoutManager instanceof GridLayoutManager) {
this.visibleThreshold = 5 * Math.max(3, ((GridLayoutManager) layoutManager).getSpanCount());
} else if (layoutManager instanceof LinearLayoutManager) {
this.visibleThreshold = ((LinearLayoutManager) layoutManager).getReverseLayout() ? 4 : 8;
} else {
this.visibleThreshold = 5;
}
}
@Override
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
final int totalItemCount = layoutManager.getItemCount();
if (totalItemCount < previousTotalItemCount) {
currentPage = 0;
previousTotalItemCount = totalItemCount;
if (totalItemCount == 0) loading = true;
}
if (loading && totalItemCount > previousTotalItemCount) {
loading = false;
previousTotalItemCount = totalItemCount;
}
final int lastVisibleItemPosition;
if (layoutManager instanceof GridLayoutManager) {
final GridLayoutManager layoutManager = (GridLayoutManager) this.layoutManager;
lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();
} else {
final LinearLayoutManager layoutManager = (LinearLayoutManager) this.layoutManager;
lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();
}
if (!loading && lastVisibleItemPosition + visibleThreshold > totalItemCount) {
if (lazyLoadListener != null) lazyLoadListener.onLoadMore(++currentPage, totalItemCount);
loading = true;
}
}
public void resetState() {
this.currentPage = 0;
this.previousTotalItemCount = 0;
this.loading = true;
}
}

34
app/src/main/java/awais/instagrabber/customviews/helpers/SwipeGestureListener.java

@ -0,0 +1,34 @@
package awais.instagrabber.customviews.helpers;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import awais.instagrabber.interfaces.SwipeEvent;
public final class SwipeGestureListener extends GestureDetector.SimpleOnGestureListener {
public static final int SWIPE_THRESHOLD = 200;
public static final int SWIPE_VELOCITY_THRESHOLD = 200;
private final SwipeEvent swipeEvent;
public SwipeGestureListener(final SwipeEvent swipeEvent) {
this.swipeEvent = swipeEvent;
}
@Override
public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, final float velocityY) {
try {
final float diffY = e2.getY() - e1.getY();
final float diffX = e2.getX() - e1.getX();
final float diffXAbs = Math.abs(diffX);
if (diffXAbs > Math.abs(diffY) && diffXAbs > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) swipeEvent.onSwipe(true);
else swipeEvent.onSwipe(false);
return true;
}
} catch (final Exception e) {
Log.e("AWAISKING_APP", "", e);
}
return false;
}
}

282
app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java

@ -0,0 +1,282 @@
package awais.instagrabber.customviews.helpers;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.net.Uri;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.activities.CommentsViewer;
import awais.instagrabber.adapters.FeedAdapter;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.settingsHelper;
// wasted around 3 hours to get this working, made from scrach, forgot to take a shower so i'm gonna go take a shower (time: May 11, 2020 @ 8:09:30 PM)
public class VideoAwareRecyclerScroller extends RecyclerView.OnScrollListener {
private static final Object LOCK = new Object();
private LinearLayoutManager layoutManager;
private View firstItemView, lastItemView;
private int videoPosShown = -1, lastVideoPos = -1, lastChangedVideoPos, lastStoppedVideoPos, lastPlayedVideoPos;
private boolean videoAttached = false;
private final List<FeedModel> feedModels;
////////////////////////////////////////////////////
private SimpleExoPlayer player;
private ImageView btnMute;
private final Context context;
private final View.OnClickListener commentClickListener = new View.OnClickListener() {
@Override
public void onClick(@NonNull final View v) {
final Object tag = v.getTag();
if (tag instanceof FeedModel && context instanceof Activity) {
if (player != null) player.setPlayWhenReady(false);
((Activity) context).startActivityForResult(new Intent(context, CommentsViewer.class)
.putExtra(Constants.EXTRAS_SHORTCODE, ((FeedModel) tag).getShortCode()), 6969);
}
}
};
private final View.OnClickListener muteClickListener = v -> {
if (player == null) return;
final float intVol = player.getVolume() == 0f ? 1f : 0f;
player.setVolume(intVol);
if (btnMute != null) btnMute.setImageResource(intVol == 0f ? R.drawable.vol : R.drawable.mute);
Utils.sessionVolumeFull = intVol == 1f;
};
private final VideoChangeCallback videoChangeCallback;
// private final ScrollerVideoCallback videoCallback;
// private View lastVideoHolder;
// private int videoState = -1;
public VideoAwareRecyclerScroller(final Context context, final List<FeedModel> feedModels,
final VideoChangeCallback videoChangeCallback) {
this.context = context;
this.feedModels = feedModels;
this.videoChangeCallback = videoChangeCallback;
}
@Override
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
if (layoutManager == null) {
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) this.layoutManager = (LinearLayoutManager) layoutManager;
}
if (feedModels.size() > 0 && layoutManager != null) {
int firstVisibleItemPos = layoutManager.findFirstCompletelyVisibleItemPosition();
int lastVisibleItemPos = layoutManager.findLastCompletelyVisibleItemPosition();
if (firstVisibleItemPos == -1 && lastVisibleItemPos == -1) {
firstVisibleItemPos = layoutManager.findFirstVisibleItemPosition();
lastVisibleItemPos = layoutManager.findLastVisibleItemPosition();
}
boolean processFirstItem = false, processLastItem = false;
View currView;
if (firstVisibleItemPos != -1) {
currView = layoutManager.findViewByPosition(firstVisibleItemPos);
if (currView != null && currView.getId() == R.id.videoHolder) {
firstItemView = currView;
processFirstItem = true;
}
}
if (lastVisibleItemPos != -1) {
currView = layoutManager.findViewByPosition(lastVisibleItemPos);
if (currView != null && currView.getId() == R.id.videoHolder) {
lastItemView = currView;
processLastItem = true;
}
}
final Rect visibleItemRect = new Rect();
int firstVisibleItemHeight = 0, lastVisibleItemHeight = 0;
final boolean isFirstItemVideoHolder = firstItemView != null && firstItemView.getId() == R.id.videoHolder;
if (isFirstItemVideoHolder) {
firstItemView.getGlobalVisibleRect(visibleItemRect);
firstVisibleItemHeight = visibleItemRect.height();
}
final boolean isLastItemVideoHolder = lastItemView != null && lastItemView.getId() == R.id.videoHolder;
if (isLastItemVideoHolder) {
lastItemView.getGlobalVisibleRect(visibleItemRect);
lastVisibleItemHeight = visibleItemRect.height();
}
if (processFirstItem && firstVisibleItemHeight > lastVisibleItemHeight) videoPosShown = firstVisibleItemPos;
else if (processLastItem && lastVisibleItemHeight != 0) videoPosShown = lastVisibleItemPos;
if (firstItemView != lastItemView) {
final int mox = lastVisibleItemHeight - firstVisibleItemHeight;
if (processLastItem && lastVisibleItemHeight > firstVisibleItemHeight) videoPosShown = lastVisibleItemPos;
if ((processFirstItem || processLastItem) && mox >= 0) videoPosShown = lastVisibleItemPos;
}
if (lastChangedVideoPos != -1 && lastVideoPos != -1) {
currView = layoutManager.findViewByPosition(lastChangedVideoPos);
if (currView != null && currView.getId() == R.id.videoHolder &&
lastStoppedVideoPos != lastChangedVideoPos && lastPlayedVideoPos != lastChangedVideoPos) {
lastStoppedVideoPos = lastChangedVideoPos;
stopVideo(lastChangedVideoPos, recyclerView, currView);
}
currView = layoutManager.findViewByPosition(lastVideoPos);
if (currView != null && currView.getId() == R.id.videoHolder) {
final Rect rect = new Rect();
currView.getGlobalVisibleRect(rect);
final int holderTop = currView.getTop();
final int holderHeight = currView.getBottom() - holderTop;
final int halfHeight = holderHeight / 2;
//halfHeight -= halfHeight / 5;
if (rect.height() < halfHeight) {
if (lastStoppedVideoPos != lastVideoPos) {
lastStoppedVideoPos = lastVideoPos;
stopVideo(lastVideoPos, recyclerView, currView);
}
} else if (lastPlayedVideoPos != lastVideoPos) {
lastPlayedVideoPos = lastVideoPos;
playVideo(lastVideoPos, recyclerView, currView);
}
}
if (lastChangedVideoPos != lastVideoPos) lastChangedVideoPos = lastVideoPos;
}
if (lastVideoPos != -1 && lastVideoPos != videoPosShown) {
if (videoAttached) {
//if ((currView = layoutManager.findViewByPosition(lastVideoPos)) != null && currView.getId() == R.id.videoHolder)
releaseVideo(lastVideoPos, recyclerView, null);
videoAttached = false;
}
}
if (videoPosShown != -1) {
lastVideoPos = videoPosShown;
if (!videoAttached) {
if ((currView = layoutManager.findViewByPosition(videoPosShown)) != null && currView.getId() == R.id.videoHolder)
attachVideo(videoPosShown, recyclerView, currView);
videoAttached = true;
}
}
}
}
private synchronized void attachVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) {
synchronized (LOCK) {
if (recyclerView != null) {
final RecyclerView.Adapter<?> adapter = recyclerView.getAdapter();
if (adapter instanceof FeedAdapter) {
final SimpleExoPlayer pagerPlayer = ((FeedAdapter) adapter).pagerPlayer;
if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(false);
}
}
if (player != null) {
player.stop(true);
player.release();
player = null;
}
player = new SimpleExoPlayer.Builder(context).build();
if (itemView != null) {
final Object tag = itemView.getTag();
final View btnComments = itemView.findViewById(R.id.btnComments);
if (btnComments != null && tag instanceof FeedModel) {
final FeedModel feedModel = (FeedModel) tag;
if (feedModel.getCommentsCount() <= 0) btnComments.setEnabled(false);
else {
btnComments.setTag(feedModel);
btnComments.setEnabled(true);
btnComments.setOnClickListener(commentClickListener);
}
}
final PlayerView playerView = itemView.findViewById(R.id.playerView);
if (playerView == null) return;
playerView.setPlayer(player);
if (player != null) {
btnMute = itemView.findViewById(R.id.btnMute);
float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f;
if (vol == 0f && Utils.sessionVolumeFull) vol = 1f;
player.setVolume(vol);
if (btnMute != null) {
btnMute.setVisibility(View.VISIBLE);
btnMute.setImageResource(vol == 0f ? R.drawable.vol : R.drawable.mute);
btnMute.setOnClickListener(muteClickListener);
}
player.setPlayWhenReady(settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS));
final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(new DefaultDataSourceFactory(context, "instagram"))
.createMediaSource(Uri.parse(feedModels.get(itemPos).getDisplayUrl()));
player.setRepeatMode(Player.REPEAT_MODE_ALL);
player.prepare(mediaSource);
player.setVolume(vol);
playerView.setOnClickListener(muteClickListener);
}
}
if (videoChangeCallback != null) videoChangeCallback.playerChanged(itemPos, player);
}
}
private void releaseVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) {
// Log.d("AWAISKING_APP", "release: " + itemPos);
// if (player != null) {
// player.stop(true);
// player.release();
// }
// player = null;
}
private void playVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) {
// if (player != null) {
// final int playbackState = player.getPlaybackState();
// if (!player.isPlaying()
// || playbackState == Player.STATE_READY || playbackState == Player.STATE_ENDED
// ) {
// player.setPlayWhenReady(true);
// }
// }
// if (player != null) {
// player.setPlayWhenReady(true);
// player.getPlaybackState();
// }
}
private void stopVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) {
if (player != null) {
player.setPlayWhenReady(false);
player.getPlaybackState();
}
}
public interface VideoChangeCallback {
void playerChanged(final int itemPos, final SimpleExoPlayer player);
}
}

252
app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/SoundParser.java

@ -0,0 +1,252 @@
package awais.instagrabber.customviews.masoudss_waveform;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.nfc.FormatException;
import android.os.Build;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
final class SoundParser {
private ProgressListener progressListener;
int[] frameGains;
//////////////////
private static String[] supportedExtensions = {"mp3", "wav", "3gpp", "3gp", "amr", "aac", "m4a", "ogg"};
private static ArrayList<String> additionalExtensions = new ArrayList<>();
static void addCustomExtension(final String extension) {
additionalExtensions.add(extension);
}
static void removeCustomExtension(final String extension) {
additionalExtensions.remove(extension);
}
static void addCustomExtensions(final List<String> extensions) {
additionalExtensions.addAll(extensions);
}
static void removeCustomExtensions(final List<String> extensions) {
additionalExtensions.removeAll(extensions);
}
private static boolean isFilenameSupported(final String filename) {
for (final String supportedExtension : supportedExtensions)
if (filename.endsWith('.' + supportedExtension)) return true;
for (final String additionalExtension : additionalExtensions)
if (filename.endsWith('.' + additionalExtension)) return true;
return false;
}
@NonNull
public static SoundParser create(final String fileName, final boolean ignoreExtension) throws IOException, FormatException {
if (!ignoreExtension && !isFilenameSupported(fileName))
throw new FormatException("Not supported file extension.");
final File f = new File(fileName);
if (!f.exists()) throw new FileNotFoundException(fileName);
final SoundParser soundFile = new SoundParser();
soundFile.readFile(f);
return soundFile;
}
public void setProgressListener(final ProgressListener progressListener) {
this.progressListener = progressListener;
}
@SuppressWarnings("deprecation")
private void readFile(@NonNull final File inputFile) throws IOException, FormatException {
final MediaExtractor extractor = new MediaExtractor();
MediaFormat format = null;
final int fileSizeBytes = (int) inputFile.length();
extractor.setDataSource(inputFile.getPath());
final int numTracks = extractor.getTrackCount();
int i = 0;
while (i < numTracks) {
format = extractor.getTrackFormat(i);
if (Objects.requireNonNull(format.getString(MediaFormat.KEY_MIME)).startsWith("audio/")) {
extractor.selectTrack(i);
break;
}
i++;
}
if (i == numTracks) throw new FormatException("No audio track found in " + inputFile);
assert format != null;
final int channels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
final int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
final int expectedNumSamples = (int) (format.getLong(MediaFormat.KEY_DURATION) / 1000000f * sampleRate + 0.5f);
final MediaCodec codec = MediaCodec.createDecoderByType(Objects.requireNonNull(format.getString(MediaFormat.KEY_MIME)));
codec.configure(format, null, null, 0);
codec.start();
final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
final ByteBuffer[] inputBuffers = codec.getInputBuffers();
boolean firstSampleData = true, doneReading = false;
long presentationTime;
int sampleSize, decodedSamplesSize = 0, totSizeRead = 0;
byte[] decodedSamples = null;
ByteBuffer mDecodedBytes = ByteBuffer.allocate(1 << 20);
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
while (true) {
final int inputBufferIndex = codec.dequeueInputBuffer(100);
if (!doneReading && inputBufferIndex >= 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
sampleSize = extractor.readSampleData(Objects.requireNonNull(codec.getInputBuffer(inputBufferIndex)), 0);
else
sampleSize = extractor.readSampleData(inputBuffers[inputBufferIndex], 0);
if (firstSampleData && sampleSize == 2 && "audio/mp4a-latm".equals(format.getString(MediaFormat.KEY_MIME))) {
extractor.advance();
totSizeRead += sampleSize;
} else if (sampleSize < 0) {
codec.queueInputBuffer(inputBufferIndex, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
doneReading = true;
} else {
presentationTime = extractor.getSampleTime();
codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTime, 0);
extractor.advance();
totSizeRead += sampleSize;
if (progressListener != null && !progressListener.reportProgress((double) totSizeRead / fileSizeBytes)) {
// We are asked to stop reading the file. Returning immediately.
// The SoundFile object is invalid and should NOT be used afterward!
extractor.release();
codec.stop();
codec.release();
return;
}
}
firstSampleData = false;
}
// Get decoded stream from the decoder output buffers.
final int outputBufferIndex = codec.dequeueOutputBuffer(info, 100);
if (outputBufferIndex >= 0 && info.size > 0) {
if (decodedSamplesSize < info.size) {
decodedSamplesSize = info.size;
decodedSamples = new byte[decodedSamplesSize];
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
final ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferIndex);
assert outputBuffer != null;
outputBuffer.get(decodedSamples, 0, info.size);
outputBuffer.clear();
} else {
outputBuffers[outputBufferIndex].get(decodedSamples, 0, info.size);
outputBuffers[outputBufferIndex].clear();
}
// Check if buffer is big enough. Resize it if it's too small.
if (mDecodedBytes.remaining() < info.size) {
// Getting a rough estimate of the total size, allocate 20% more, and
// make sure to allocate at least 5MB more than the initial size.
final int position = mDecodedBytes.position();
int newSize = (int) (position * (1.0 * fileSizeBytes / totSizeRead) * 1.2);
final int infoSize = info.size + 5 * (1 << 20);
if (newSize - position < infoSize)
newSize = position + infoSize;
ByteBuffer newDecodedBytes = null;
// Try to allocate memory. If we are OOM, try to run the garbage collector.
int retry = 10;
while (retry > 0) {
try {
newDecodedBytes = ByteBuffer.allocate(newSize);
break;
} catch (final OutOfMemoryError e) {
retry--;
}
}
if (retry == 0) break;
mDecodedBytes.rewind();
assert newDecodedBytes != null;
newDecodedBytes.put(mDecodedBytes);
mDecodedBytes = newDecodedBytes;
mDecodedBytes.position(position);
}
mDecodedBytes.put(decodedSamples, 0, info.size);
codec.releaseOutputBuffer(outputBufferIndex, false);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED)
outputBuffers = codec.getOutputBuffers();
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0 || mDecodedBytes.position() / (2 * channels) >= expectedNumSamples)
break;
}
final int numSamples = mDecodedBytes.position() / (channels * 2); // One sample = 2 bytes.
mDecodedBytes.rewind();
mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN);
final ShortBuffer mDecodedSamples = mDecodedBytes.asShortBuffer();
// final int avgBitrateKbps = (int) (fileSizeBytes * 8F * ((float) sampleRate / numSamples) / 1000F);
extractor.release();
codec.stop();
codec.release();
final int samplesPerFrame = 1024;
int numFrames = numSamples / samplesPerFrame;
if (numSamples % samplesPerFrame != 0) numFrames++;
frameGains = new int[numFrames];
// final int[] mFrameLens = new int[numFrames];
// final int[] mFrameOffsets = new int[numFrames];
// final int frameLens = (int) (1000F * avgBitrateKbps / 8F * ((float) samplesPerFrame / sampleRate));
int j, gain, value;
i = 0;
while (i < numFrames) {
gain = -1;
j = 0;
while (j < samplesPerFrame) {
value = 0;
for (int k = 0; k < channels; ++k)
if (mDecodedSamples.remaining() > 0)
value += Math.abs(mDecodedSamples.get());
value /= channels;
if (gain < value) gain = value;
j++;
}
frameGains[i] = (int) Math.sqrt(gain);
// mFrameLens[i] = frameLens;
// mFrameOffsets[i] = (int) ((float) i * (1000F * avgBitrateKbps / 8F) * ((float) samplesPerFrame / sampleRate));
i++;
}
mDecodedSamples.rewind();
}
private interface ProgressListener {
boolean reportProgress(final double fractionComplete);
}
}

5
app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveFormProgressChangeListener.java

@ -0,0 +1,5 @@
package awais.instagrabber.customviews.masoudss_waveform;
public interface WaveFormProgressChangeListener {
void onProgressChanged(final WaveformSeekBar waveformSeekBar, final int progress, final boolean fromUser);
}

7
app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveGravity.java

@ -0,0 +1,7 @@
package awais.instagrabber.customviews.masoudss_waveform;
public enum WaveGravity {
TOP,
CENTER,
BOTTOM,
}

225
app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveformSeekBar.java

@ -0,0 +1,225 @@
package awais.instagrabber.customviews.masoudss_waveform;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import awais.instagrabber.R;
import awais.instagrabber.utils.Utils;
public final class WaveformSeekBar extends View {
private final int mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
private final Paint mWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final RectF mWaveRect = new RectF();
private final Canvas mProgressCanvas = new Canvas();
private final WaveGravity waveGravity = WaveGravity.BOTTOM;
private final int waveBackgroundColor;
private final int waveProgressColor;
private final float waveWidth = Utils.convertDpToPx(3);
private final float waveMinHeight = Utils.convertDpToPx(4);
private final float waveCornerRadius = Utils.convertDpToPx(2);
private final float waveGap = Utils.convertDpToPx(1);
private int mCanvasWidth = 0;
private int mCanvasHeight = 0;
private float mTouchDownX = 0F;
private int[] sample;
private int progress = 0;
private WaveFormProgressChangeListener progressChangeListener;
public WaveformSeekBar(final Context context) {
this(context, null);
}
public WaveformSeekBar(final Context context, @Nullable final AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveformSeekBar(final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.waveBackgroundColor = ContextCompat.getColor(context, R.color.text_color_light);
this.waveProgressColor = ContextCompat.getColor(context, R.color.text_color_dark);
}
private int getSampleMax() {
int max = -1;
if (sample != null) for (final int i : sample) if (i >= max) max = i;
return max;
}
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
if (sample != null && sample.length != 0) {
final int availableWidth = getAvailableWidth();
final int availableHeight = getAvailableHeight();
final float step = availableWidth / (waveGap + waveWidth) / sample.length;
float i = 0F;
float lastWaveRight = (float) getPaddingLeft();
final int sampleMax = getSampleMax();
while (i < sample.length) {
float waveHeight = availableHeight * ((float) sample[(int) i] / sampleMax);
if (waveHeight < waveMinHeight)
waveHeight = waveMinHeight;
final float top;
if (waveGravity == WaveGravity.TOP) {
top = (float) getPaddingTop();
} else if (waveGravity == WaveGravity.CENTER) {
top = (float) getPaddingTop() + availableHeight / 2F - waveHeight / 2F;
} else if (waveGravity == WaveGravity.BOTTOM) {
top = mCanvasHeight - (float) getPaddingBottom() - waveHeight;
} else {
top = 0;
}
mWaveRect.set(lastWaveRight, top, lastWaveRight + waveWidth, top + waveHeight);
if (mWaveRect.contains(availableWidth * progress / 100F, mWaveRect.centerY())) {
int bitHeight = (int) mWaveRect.height();
if (bitHeight <= 0) bitHeight = (int) waveWidth;
final Bitmap bitmap = Bitmap.createBitmap(availableWidth, bitHeight, Bitmap.Config.ARGB_8888);
mProgressCanvas.setBitmap(bitmap);
float fillWidth = availableWidth * progress / 100F;
mWavePaint.setColor(waveProgressColor);
mProgressCanvas.drawRect(0F, 0F, fillWidth, mWaveRect.bottom, mWavePaint);
mWavePaint.setColor(waveBackgroundColor);
mProgressCanvas.drawRect(fillWidth, 0F, (float) availableWidth, mWaveRect.bottom, mWavePaint);
mWavePaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
} else {
mWavePaint.setColor(mWaveRect.right <= availableWidth * progress / 100F ? waveProgressColor : waveBackgroundColor);
mWavePaint.setShader(null);
}
canvas.drawRoundRect(mWaveRect, waveCornerRadius, waveCornerRadius, mWavePaint);
lastWaveRight = mWaveRect.right + waveGap;
if (lastWaveRight + waveWidth > availableWidth + getPaddingLeft())
break;
i += 1 / step;
}
}
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
if (!isEnabled()) return false;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (isParentScrolling()) mTouchDownX = event.getX();
else updateProgress(event);
break;
case MotionEvent.ACTION_MOVE:
updateProgress(event);
break;
case MotionEvent.ACTION_UP:
if (Math.abs(event.getX() - mTouchDownX) > mScaledTouchSlop)
updateProgress(event);
performClick();
break;
}
return true;
}
@Override
protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mCanvasWidth = w;
mCanvasHeight = h;
}
@Override
public boolean performClick() {
super.performClick();
return true;
}
private boolean isParentScrolling() {
View parent = (View) getParent();
final View root = getRootView();
while (true) {
if (parent.canScrollHorizontally(1) || parent.canScrollHorizontally(-1) ||
parent.canScrollVertically(1) || parent.canScrollVertically(-1))
return true;
if (parent == root) return false;
parent = (View) parent.getParent();
}
}
private void updateProgress(@NonNull final MotionEvent event) {
progress = (int) (100 * event.getX() / getAvailableWidth());
invalidate();
if (progressChangeListener != null)
progressChangeListener.onProgressChanged(this, Math.min(Math.max(0, progress), 100), true);
}
private int getAvailableWidth() {
return mCanvasWidth - getPaddingLeft() - getPaddingRight();
}
private int getAvailableHeight() {
return mCanvasHeight - getPaddingTop() - getPaddingBottom();
}
// public void setSampleFrom(final String path, final boolean ignoreExtension) { // was false
// try {
// final SoundParser soundFile = SoundParser.create(path, ignoreExtension);
// sample = soundFile.frameGains;
// } catch (final Exception e) {
// sample = null;
// }
// }
//
// public void setSampleFrom(@NonNull final File file, final boolean ignoreExtension) { // was false
// setSampleFrom(file.getAbsolutePath(), ignoreExtension);
// }
public void setProgress(final int progress) {
this.progress = progress;
invalidate();
}
public void setProgressChangeListener(final WaveFormProgressChangeListener progressChangeListener) {
this.progressChangeListener = progressChangeListener;
}
public void setSample(final int[] sample) {
if (sample != this.sample) {
this.sample = sample;
invalidate();
}
}
}

98
app/src/main/java/awais/instagrabber/dialogs/AboutDialog.java

@ -0,0 +1,98 @@
package awais.instagrabber.dialogs;
import android.app.Dialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.method.LinkMovementMethod;
import android.text.style.RelativeSizeSpan;
import android.text.style.URLSpan;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.LinearLayoutCompat;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import awais.instagrabber.R;
import awais.instagrabber.utils.Utils;
public final class AboutDialog extends BottomSheetDialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
final Dialog dialog = super.onCreateDialog(savedInstanceState);
final View contentView = View.inflate(getContext(), R.layout.dialog_main_about, null);
final LinearLayoutCompat infoContainer = contentView.findViewById(R.id.infoContainer);
final View btnTelegram = infoContainer.getChildAt(1);
final View btnProject = infoContainer.getChildAt(2);
final View.OnClickListener onClickListener = v -> {
final Intent intent = new Intent(Intent.ACTION_VIEW);
if (v == btnTelegram) {
intent.setData(Uri.parse("https://t.me/grabber_app"));
if (!Utils.isEmpty(Utils.telegramPackage))
intent.setPackage(Utils.telegramPackage);
} else
intent.setData(Uri.parse("https://gitlab.com/AwaisKing/instagrabber/"));
startActivity(intent);
};
btnProject.setOnClickListener(onClickListener);
btnTelegram.setOnClickListener(onClickListener);
final String description = getString(R.string.description);
if (!Utils.isEmpty(description)) {
final SpannableStringBuilder descriptionText = new SpannableStringBuilder(description, 0, description.length());
int lastIndex = descriptionText.length() / 2;
for (int i = 0; i < descriptionText.length(); ++i) {
char c = descriptionText.charAt(i);
if (c == '[') {
final int smallTextStart = i;
descriptionText.delete(i, i + 1);
do {
c = descriptionText.charAt(i);
if (c == ']') {
descriptionText.delete(i, i + 1);
descriptionText.setSpan(new RelativeSizeSpan(0.5f), smallTextStart, i, 0);
}
++i;
} while (c != ']' || i == descriptionText.length() - 1);
} else if (c == '{') {
final int smallerTextStart = i;
descriptionText.delete(i, i + 1);
i = smallerTextStart;
do {
c = descriptionText.charAt(i);
if (c == '}') {
descriptionText.delete(i, i + 1);
descriptionText.setSpan(new RelativeSizeSpan(0.35f), smallerTextStart, i, 0);
}
++i;
lastIndex = i;
} while (c != '}' || i == descriptionText.length() - 1);
}
}
lastIndex = Utils.indexOfChar(descriptionText, '@', lastIndex);
descriptionText.setSpan(new URLSpan("https://t.me/awais404"), lastIndex, lastIndex + 9, 0);
lastIndex = Utils.indexOfChar(descriptionText, ':', lastIndex + 9) + 2;
descriptionText.setSpan(new URLSpan("mailto:[email protected]"), lastIndex, lastIndex + 24, 0);
final TextView textView = (TextView) infoContainer.getChildAt(0);
textView.setMovementMethod(new LinkMovementMethod());
textView.setText(descriptionText, TextView.BufferType.SPANNABLE);
}
dialog.setContentView(contentView);
return dialog;
}
}

62
app/src/main/java/awais/instagrabber/dialogs/ProfileSettingsDialog.java

@ -0,0 +1,62 @@
package awais.instagrabber.dialogs;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import awais.instagrabber.R;
import static awais.instagrabber.utils.Constants.PROFILE_FETCH_MODE;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class ProfileSettingsDialog extends BottomSheetDialogFragment implements AdapterView.OnItemSelectedListener {
private int fetchIndex;
private Activity activity;
private Spinner spProfileFetchMode;
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final Dialog dialog = super.onCreateDialog(savedInstanceState);
final Context context = getContext();
activity = context instanceof Activity ? (Activity) context : getActivity();
final View contentView = View.inflate(activity, R.layout.dialog_profile_settings, null);
spProfileFetchMode = contentView.findViewById(R.id.spProfileFetchMode);
fetchIndex = Math.min(2, Math.max(0, settingsHelper.getInteger(PROFILE_FETCH_MODE)));
spProfileFetchMode.setSelection(fetchIndex);
spProfileFetchMode.setOnItemSelectedListener(this);
dialog.setContentView(contentView);
return dialog;
}
@Override
public void onDismiss(@NonNull final DialogInterface dialog) {
super.onDismiss(dialog);
if (activity != null && (spProfileFetchMode == null || fetchIndex != spProfileFetchMode.getSelectedItemPosition()))
activity.recreate();
}
@Override
public void onItemSelected(final AdapterView<?> parent, final View view, final int position, final long id) {
settingsHelper.putInteger(PROFILE_FETCH_MODE, position);
}
@Override
public void onNothingSelected(final AdapterView<?> parent) { }
}

169
app/src/main/java/awais/instagrabber/dialogs/QuickAccessDialog.java

@ -0,0 +1,169 @@
package awais.instagrabber.dialogs;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import java.util.ArrayList;
import awais.instagrabber.R;
import awais.instagrabber.activities.Main;
import awais.instagrabber.adapters.SimpleAdapter;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DataBox;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class QuickAccessDialog extends BottomSheetDialogFragment implements DialogInterface.OnShowListener,
View.OnClickListener, View.OnLongClickListener {
private boolean cookieChanged, isQuery;
private Activity activity;
private String userQuery;
private View btnFavorite, btnImportExport;
private SimpleAdapter<DataBox.FavoriteModel> favoritesAdapter;
public QuickAccessDialog setQuery(final String userQuery) {
this.userQuery = userQuery;
return this;
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.setOnShowListener(this);
final Context context = getContext();
activity = context instanceof Activity ? (Activity) context : getActivity();
final View contentView = View.inflate(activity, R.layout.dialog_quick_access, null);
btnFavorite = contentView.findViewById(R.id.btnFavorite);
btnImportExport = contentView.findViewById(R.id.importExport);
isQuery = !Utils.isEmpty(userQuery);
btnFavorite.setVisibility(isQuery ? View.VISIBLE : View.GONE);
Utils.setTooltipText(btnImportExport, R.string.import_export);
favoritesAdapter = new SimpleAdapter<>(activity, Utils.dataBox.getAllFavorites(), this, this);
btnFavorite.setOnClickListener(this);
btnImportExport.setOnClickListener(this);
final RecyclerView rvFavorites = contentView.findViewById(R.id.rvFavorites);
final RecyclerView rvQuickAccess = contentView.findViewById(R.id.rvQuickAccess);
final DividerItemDecoration itemDecoration = new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL);
rvFavorites.addItemDecoration(itemDecoration);
rvFavorites.setAdapter(favoritesAdapter);
final String cookieStr = settingsHelper.getString(Constants.COOKIE);
if (!Utils.isEmpty(cookieStr)
|| Utils.dataBox.getCookieCount() > 0 // fallback for export / import
) {
rvQuickAccess.addItemDecoration(itemDecoration);
final ArrayList<DataBox.CookieModel> allCookies = Utils.dataBox.getAllCookies();
if (!Utils.isEmpty(cookieStr) && allCookies != null) {
for (final DataBox.CookieModel cookie : allCookies) {
if (cookieStr.equals(cookie.getCookie())) {
cookie.setSelected(true);
break;
}
}
}
rvQuickAccess.setAdapter(new SimpleAdapter<>(activity, allCookies, this, this));
} else {
((View) rvQuickAccess.getParent()).setVisibility(View.GONE);
}
dialog.setContentView(contentView);
return dialog;
}
@Override
public void onClick(@NonNull final View v) {
final Object tag = v.getTag();
if (v == btnFavorite) {
if (isQuery) {
Utils.dataBox.addFavorite(new DataBox.FavoriteModel(userQuery, System.currentTimeMillis()));
favoritesAdapter.setItems(Utils.dataBox.getAllFavorites());
}
} else if (v == btnImportExport) {
if (ContextCompat.checkSelfPermission(activity, Utils.PERMS[0]) == PackageManager.PERMISSION_DENIED)
requestPermissions(Utils.PERMS, 6007);
else Utils.showImportExportDialog(v.getContext());
} else if (tag instanceof DataBox.FavoriteModel) {
if (Main.scanHack != null) {
Main.scanHack.onResult(((DataBox.FavoriteModel) tag).getQuery());
dismiss();
}
} else if (tag instanceof DataBox.CookieModel) {
final DataBox.CookieModel cookieModel = (DataBox.CookieModel) tag;
if (!cookieModel.isSelected()) {
settingsHelper.putString(Constants.COOKIE, cookieModel.getCookie());
Utils.setupCookies(cookieModel.getCookie());
cookieChanged = true;
}
dismiss();
}
}
@Override
public boolean onLongClick(@NonNull final View v) {
final Object tag = v.getTag();
if (tag instanceof DataBox.FavoriteModel) {
final DataBox.FavoriteModel favoriteModel = (DataBox.FavoriteModel) tag;
new AlertDialog.Builder(activity).setPositiveButton(R.string.yes, (d, which) -> Utils.dataBox.delFavorite(favoriteModel))
.setNegativeButton(R.string.no, null).setMessage(getString(R.string.quick_access_confirm_delete,
favoriteModel.getQuery())).show();
} else if (tag instanceof DataBox.CookieModel) {
final DataBox.CookieModel cookieModel = (DataBox.CookieModel) tag;
if (cookieModel.isSelected())
Toast.makeText(v.getContext(), R.string.quick_access_cannot_delete_curr, Toast.LENGTH_SHORT).show();
else
new AlertDialog.Builder(activity).setPositiveButton(R.string.yes, (d, which) -> Utils.dataBox.delUserCookie(cookieModel))
.setNegativeButton(R.string.no, null).setMessage(getString(R.string.quick_access_confirm_delete,
cookieModel.getUsername())).show();
}
return true;
}
@Override
public void onDismiss(@NonNull final DialogInterface dialog) {
super.onDismiss(dialog);
if (cookieChanged && activity != null) activity.recreate();
}
@Override
public void onShow(final DialogInterface dialog) {
if (settingsHelper.getBoolean(Constants.SHOW_QUICK_ACCESS_DIALOG))
new AlertDialog.Builder(activity)
.setMessage(R.string.quick_access_info_dialog)
.setPositiveButton(R.string.ok, null)
.setNeutralButton(R.string.dont_show_again, (d, which) ->
settingsHelper.putBoolean(Constants.SHOW_QUICK_ACCESS_DIALOG, false)).show();
}
}

213
app/src/main/java/awais/instagrabber/dialogs/SettingsDialog.java

@ -0,0 +1,213 @@
package awais.instagrabber.dialogs;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.CompoundButton;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatCheckBox;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import awais.instagrabber.R;
import awais.instagrabber.activities.Login;
import awais.instagrabber.utils.DirectoryChooser;
import awais.instagrabber.utils.LocaleUtils;
import awais.instagrabber.utils.Utils;
import awaisomereport.CrashReporter;
import static awais.instagrabber.utils.Constants.APP_LANGUAGE;
import static awais.instagrabber.utils.Constants.APP_THEME;
import static awais.instagrabber.utils.Constants.AUTOLOAD_POSTS;
import static awais.instagrabber.utils.Constants.AUTOPLAY_VIDEOS;
import static awais.instagrabber.utils.Constants.BOTTOM_TOOLBAR;
import static awais.instagrabber.utils.Constants.DOWNLOAD_USER_FOLDER;
import static awais.instagrabber.utils.Constants.FOLDER_PATH;
import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO;
import static awais.instagrabber.utils.Constants.MUTED_VIDEOS;
import static awais.instagrabber.utils.Constants.SHOW_FEED;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class SettingsDialog extends BottomSheetDialogFragment implements View.OnClickListener, AdapterView.OnItemSelectedListener,
CompoundButton.OnCheckedChangeListener {
private Activity activity;
private FragmentManager fragmentManager;
private View btnSaveTo, btnImportExport, btnLogin, btnTimeSettings, btnReport;
private Spinner spAppTheme, spLanguage;
private boolean somethingChanged = false;
private int currentTheme, currentLanguage, selectedLanguage;
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
if (requestCode != 6200) return;
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) showDirectoryChooser();
else Toast.makeText(activity, R.string.direct_download_perms_ask, Toast.LENGTH_SHORT).show();
}
private void showDirectoryChooser() {
FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager == null) fragmentManager = getChildFragmentManager();
new DirectoryChooser().setInitialDirectory(settingsHelper.getString(FOLDER_PATH))
.setInteractionListener(path -> {
settingsHelper.putString(FOLDER_PATH, path);
somethingChanged = true;
}).show(fragmentManager, null);
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
final Dialog dialog = super.onCreateDialog(savedInstanceState);
final Context context = getContext();
activity = context instanceof Activity ? (Activity) context : getActivity();
fragmentManager = getFragmentManager();
if (fragmentManager == null) fragmentManager = getChildFragmentManager();
final View contentView = View.inflate(activity, R.layout.dialog_main_settings, null);
btnLogin = contentView.findViewById(R.id.btnLogin);
btnSaveTo = contentView.findViewById(R.id.btnSaveTo);
btnImportExport = contentView.findViewById(R.id.importExport);
btnTimeSettings = contentView.findViewById(R.id.btnTimeSettings);
btnReport = contentView.findViewById(R.id.btnReport);
Utils.setTooltipText(btnImportExport, R.string.import_export);
btnLogin.setOnClickListener(this);
btnReport.setOnClickListener(this);
btnSaveTo.setOnClickListener(this);
btnImportExport.setOnClickListener(this);
btnTimeSettings.setOnClickListener(this);
spAppTheme = contentView.findViewById(R.id.spAppTheme);
currentTheme = settingsHelper.getInteger(APP_THEME);
spAppTheme.setSelection(currentTheme);
spAppTheme.setOnItemSelectedListener(this);
spLanguage = contentView.findViewById(R.id.spLanguage);
currentLanguage = settingsHelper.getInteger(APP_LANGUAGE);
spLanguage.setSelection(currentLanguage);
spLanguage.setOnItemSelectedListener(this);
final AppCompatCheckBox cbSaveTo = contentView.findViewById(R.id.cbSaveTo);
final AppCompatCheckBox cbShowFeed = contentView.findViewById(R.id.cbShowFeed);
final AppCompatCheckBox cbMuteVideos = contentView.findViewById(R.id.cbMuteVideos);
final AppCompatCheckBox cbBottomToolbar = contentView.findViewById(R.id.cbBottomToolbar);
final AppCompatCheckBox cbAutoloadPosts = contentView.findViewById(R.id.cbAutoloadPosts);
final AppCompatCheckBox cbAutoplayVideos = contentView.findViewById(R.id.cbAutoplayVideos);
final AppCompatCheckBox cbDownloadUsername = contentView.findViewById(R.id.cbDownloadUsername);
cbSaveTo.setChecked(settingsHelper.getBoolean(FOLDER_SAVE_TO));
cbMuteVideos.setChecked(settingsHelper.getBoolean(MUTED_VIDEOS));
cbBottomToolbar.setChecked(settingsHelper.getBoolean(BOTTOM_TOOLBAR));
cbAutoplayVideos.setChecked(settingsHelper.getBoolean(AUTOPLAY_VIDEOS));
cbShowFeed.setChecked(settingsHelper.getBoolean(SHOW_FEED));
cbAutoloadPosts.setChecked(settingsHelper.getBoolean(AUTOLOAD_POSTS));
cbDownloadUsername.setChecked(settingsHelper.getBoolean(DOWNLOAD_USER_FOLDER));
setupListener(cbSaveTo);
setupListener(cbShowFeed);
setupListener(cbMuteVideos);
setupListener(cbBottomToolbar);
setupListener(cbAutoloadPosts);
setupListener(cbAutoplayVideos);
setupListener(cbDownloadUsername);
btnSaveTo.setEnabled(cbSaveTo.isChecked());
dialog.setContentView(contentView);
return dialog;
}
private void setupListener(@NonNull final AppCompatCheckBox checkBox) {
checkBox.setOnCheckedChangeListener(this);
((View) checkBox.getParent()).setOnClickListener(this);
}
@Override
public void onItemSelected(final AdapterView<?> spinner, final View view, final int position, final long id) {
if (spinner == spAppTheme) {
if (position != currentTheme) {
settingsHelper.putInteger(APP_THEME, position);
somethingChanged = true;
}
} else if (spinner == spLanguage) {
selectedLanguage = position;
if (position != currentLanguage) {
settingsHelper.putInteger(APP_LANGUAGE, position);
somethingChanged = true;
}
}
}
@Override
public void onClick(final View v) {
if (v == btnLogin) {
startActivity(new Intent(v.getContext(), Login.class));
somethingChanged = true;
} else if (v == btnImportExport) {
if (ContextCompat.checkSelfPermission(activity, Utils.PERMS[0]) == PackageManager.PERMISSION_DENIED)
requestPermissions(Utils.PERMS, 6007);
else Utils.showImportExportDialog(activity);
} else if (v == btnTimeSettings) {
new TimeSettingsDialog().show(fragmentManager, null);
} else if (v == btnReport) {
CrashReporter.get(activity.getApplication()).zipLogs().startCrashEmailIntent(activity, true);
} else if (v == btnSaveTo) {
if (ContextCompat.checkSelfPermission(activity, Utils.PERMS[0]) == PackageManager.PERMISSION_DENIED)
requestPermissions(Utils.PERMS, 6200);
else showDirectoryChooser();
} else if (v instanceof ViewGroup)
((ViewGroup) v).getChildAt(0).performClick();
}
@Override
public void onCheckedChanged(@NonNull final CompoundButton checkBox, final boolean checked) {
final int id = checkBox.getId();
if (id == R.id.cbDownloadUsername) settingsHelper.putBoolean(DOWNLOAD_USER_FOLDER, checked);
else if (id == R.id.cbBottomToolbar) settingsHelper.putBoolean(BOTTOM_TOOLBAR, checked);
else if (id == R.id.cbAutoplayVideos) settingsHelper.putBoolean(AUTOPLAY_VIDEOS, checked);
else if (id == R.id.cbMuteVideos) settingsHelper.putBoolean(MUTED_VIDEOS, checked);
else if (id == R.id.cbAutoloadPosts) settingsHelper.putBoolean(AUTOLOAD_POSTS, checked);
else if (id == R.id.cbShowFeed) settingsHelper.putBoolean(SHOW_FEED, checked);
else if (id == R.id.cbSaveTo) {
settingsHelper.putBoolean(FOLDER_SAVE_TO, checked);
btnSaveTo.setEnabled(checked);
}
somethingChanged = true;
}
@Override
public void onDismiss(@NonNull final DialogInterface dialog) {
if (selectedLanguage != currentLanguage)
LocaleUtils.setLocale(activity != null ? activity.getBaseContext() : getLayoutInflater().getContext().getApplicationContext());
super.onDismiss(dialog);
if (somethingChanged && activity != null) activity.recreate();
}
@Override
public void onNothingSelected(final AdapterView<?> parent) { }
}

173
app/src/main/java/awais/instagrabber/dialogs/TimeSettingsDialog.java

@ -0,0 +1,173 @@
package awais.instagrabber.dialogs;
import android.app.Dialog;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CompoundButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import awais.instagrabber.databinding.DialogTimeSettingsBinding;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.LocaleUtils;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class TimeSettingsDialog extends DialogFragment implements AdapterView.OnItemSelectedListener, CompoundButton.OnCheckedChangeListener,
View.OnClickListener, TextWatcher {
private DialogTimeSettingsBinding timeSettingsBinding;
private final Date magicDate;
private SimpleDateFormat currentFormat;
private String selectedFormat;
public TimeSettingsDialog() {
super();
final Calendar instance = GregorianCalendar.getInstance();
instance.set(2020, 5, 22, 8, 17, 13);
magicDate = instance.getTime();
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
final Dialog dialog = super.onCreateDialog(savedInstanceState);
timeSettingsBinding = DialogTimeSettingsBinding.inflate(LayoutInflater.from(getContext()));
timeSettingsBinding.cbCustomFormat.setOnCheckedChangeListener(this);
timeSettingsBinding.cbCustomFormat.setChecked(settingsHelper.getBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED));
timeSettingsBinding.etCustomFormat.setText(settingsHelper.getString(Constants.CUSTOM_DATE_TIME_FORMAT));
final String[] dateTimeFormat = settingsHelper.getString(Constants.DATE_TIME_SELECTION).split(";"); // output = time;separator;date
timeSettingsBinding.spTimeFormat.setSelection(Integer.parseInt(dateTimeFormat[0]));
timeSettingsBinding.spSeparator.setSelection(Integer.parseInt(dateTimeFormat[1]));
timeSettingsBinding.spDateFormat.setSelection(Integer.parseInt(dateTimeFormat[2]));
timeSettingsBinding.cbSwapTimeDate.setOnCheckedChangeListener(this);
refreshTimeFormat();
timeSettingsBinding.spTimeFormat.setOnItemSelectedListener(this);
timeSettingsBinding.spDateFormat.setOnItemSelectedListener(this);
timeSettingsBinding.spSeparator.setOnItemSelectedListener(this);
timeSettingsBinding.etCustomFormat.addTextChangedListener(this);
timeSettingsBinding.btnConfirm.setOnClickListener(this);
timeSettingsBinding.btnInfo.setOnClickListener(this);
dialog.setContentView(timeSettingsBinding.getRoot());
return dialog;
}
private void refreshTimeFormat() {
if (timeSettingsBinding.cbCustomFormat.isChecked()) {
timeSettingsBinding.btnConfirm.setEnabled(false);
checkCustomTimeFormat();
} else {
final String sepStr = String.valueOf(timeSettingsBinding.spSeparator.getSelectedItem());
final String timeStr = String.valueOf(timeSettingsBinding.spTimeFormat.getSelectedItem());
final String dateStr = String.valueOf(timeSettingsBinding.spDateFormat.getSelectedItem());
final boolean isSwapTime = !timeSettingsBinding.cbSwapTimeDate.isChecked();
selectedFormat = (isSwapTime ? timeStr : dateStr)
+ (Utils.isEmpty(sepStr) || timeSettingsBinding.spSeparator.getSelectedItemPosition() == 0 ? " " : " '" + sepStr + "' ")
+ (isSwapTime ? dateStr : timeStr);
timeSettingsBinding.btnConfirm.setEnabled(true);
timeSettingsBinding.timePreview.setText((currentFormat = new SimpleDateFormat(selectedFormat, LocaleUtils.getCurrentLocale())).format(magicDate));
}
}
private void checkCustomTimeFormat() {
try {
//noinspection ConstantConditions
final String string = timeSettingsBinding.etCustomFormat.getText().toString();
if (Utils.isEmpty(string)) throw new NullPointerException();
final String format = (currentFormat = new SimpleDateFormat(string, LocaleUtils.getCurrentLocale())).format(magicDate);
timeSettingsBinding.timePreview.setText(format);
timeSettingsBinding.btnConfirm.setEnabled(true);
} catch (final Exception e) {
timeSettingsBinding.btnConfirm.setEnabled(false);
timeSettingsBinding.timePreview.setText(null);
}
}
@Override
public void onItemSelected(final AdapterView<?> p, final View v, final int pos, final long id) {
refreshTimeFormat();
}
@Override
public void onCheckedChanged(final CompoundButton buttonView, final boolean isChecked) {
if (buttonView == timeSettingsBinding.cbCustomFormat) {
timeSettingsBinding.etCustomFormat.setEnabled(isChecked);
timeSettingsBinding.btnInfo.setEnabled(isChecked);
timeSettingsBinding.spTimeFormat.setEnabled(!isChecked);
timeSettingsBinding.spDateFormat.setEnabled(!isChecked);
timeSettingsBinding.spSeparator.setEnabled(!isChecked);
timeSettingsBinding.cbSwapTimeDate.setEnabled(!isChecked);
}
refreshTimeFormat();
}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
checkCustomTimeFormat();
}
@Override
public void onClick(final View v) {
if (v == timeSettingsBinding.btnConfirm) {
final String formatSelection;
final boolean isCustomFormat = timeSettingsBinding.cbCustomFormat.isChecked();
if (isCustomFormat) {
//noinspection ConstantConditions
formatSelection = timeSettingsBinding.etCustomFormat.getText().toString();
settingsHelper.putString(Constants.CUSTOM_DATE_TIME_FORMAT, formatSelection);
} else {
formatSelection = timeSettingsBinding.spTimeFormat.getSelectedItemPosition() + ";"
+ timeSettingsBinding.spSeparator.getSelectedItemPosition() + ';'
+ timeSettingsBinding.spDateFormat.getSelectedItemPosition(); // time;separator;date
settingsHelper.putString(Constants.DATE_TIME_FORMAT, selectedFormat);
settingsHelper.putString(Constants.DATE_TIME_SELECTION, formatSelection);
}
settingsHelper.putBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED, isCustomFormat);
Utils.datetimeParser = (SimpleDateFormat) currentFormat.clone();
dismiss();
} else if (v == timeSettingsBinding.btnInfo) {
timeSettingsBinding.customPanel.setVisibility(timeSettingsBinding.customPanel
.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
}
}
@Override
public void onNothingSelected(final AdapterView<?> parent) { }
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { }
@Override
public void afterTextChanged(final Editable s) { }
}

153
app/src/main/java/awais/instagrabber/directdownload/DirectDownload.java

@ -0,0 +1,153 @@
package awais.instagrabber.directdownload;
import android.Manifest;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
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.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import java.util.Arrays;
import awais.instagrabber.R;
import awais.instagrabber.asyncs.PostFetcher;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.IntentModel;
import awais.instagrabber.models.ViewerPostModel;
import awais.instagrabber.models.enums.DownloadMethod;
import awais.instagrabber.models.enums.IntentModelType;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.CHANNEL_ID;
import static awais.instagrabber.utils.Utils.CHANNEL_NAME;
import static awais.instagrabber.utils.Utils.isChannelCreated;
import static awais.instagrabber.utils.Utils.notificationManager;
public final class DirectDownload extends Activity {
private boolean isFound = false;
private Intent intent;
private Context context;
@Override
public void onWindowAttributesChanged(final WindowManager.LayoutParams params) {
super.onWindowAttributesChanged(params);
if (!isFound) {
intent = getIntent();
context = getApplicationContext();
if (intent != null && context != null) {
isFound = true;
checkIntent();
}
}
}
@Override
public Resources getResources() {
if (!isFound) {
intent = getIntent();
context = getApplicationContext();
if (intent != null && context != null) {
isFound = true;
checkIntent();
}
}
return super.getResources();
}
private synchronized void checkIntent() {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
doDownload();
else {
final Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, R.string.direct_download_perms_ask, Toast.LENGTH_LONG).show();
handler.removeCallbacks(this);
}
});
ActivityCompat.requestPermissions(this, Utils.PERMS, 8020);
}
finish();
}
private synchronized void doDownload() {
final String action = intent.getAction();
if (!Utils.isEmpty(action) && !Intent.ACTION_MAIN.equals(action)) {
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 && !Utils.isEmpty(data)) {
final IntentModel model = Utils.stripString(data);
if (model != null && model.getType() == IntentModelType.POST) {
final String text = model.getText();
new PostFetcher(text, new FetchListener<ViewerPostModel[]>() {
@Override
public void doBefore() {
if (notificationManager == null)
notificationManager = NotificationManagerCompat.from(context.getApplicationContext());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isChannelCreated) {
notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH));
isChannelCreated = true;
}
final Notification fetchingPostNotif = new NotificationCompat.Builder(context, CHANNEL_ID)
.setCategory(NotificationCompat.CATEGORY_STATUS).setSmallIcon(R.mipmap.ic_launcher)
.setAutoCancel(false).setPriority(NotificationCompat.PRIORITY_MIN)
.setContentText(context.getString(R.string.direct_download_loading)).build();
notificationManager.notify(1900000000, fetchingPostNotif);
}
@Override
public void onResult(final ViewerPostModel[] result) {
if (notificationManager != null) notificationManager.cancel(1900000000);
if (result != null) {
if (result.length == 1) {
Utils.batchDownload(context, result[0].getUsername(), DownloadMethod.DOWNLOAD_DIRECT,
Arrays.asList(result));
} else if (result.length > 1) {
context.startActivity(new Intent(context, MultiDirectDialog.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
.putExtra(Constants.EXTRAS_POST, result));
}
}
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
}
}
}

117
app/src/main/java/awais/instagrabber/directdownload/MultiDirectDialog.java

@ -0,0 +1,117 @@
package awais.instagrabber.directdownload;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Collections;
import awais.instagrabber.R;
import awais.instagrabber.activities.BaseLanguageActivity;
import awais.instagrabber.adapters.PostsAdapter;
import awais.instagrabber.customviews.helpers.GridAutofitLayoutManager;
import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration;
import awais.instagrabber.models.BasePostModel;
import awais.instagrabber.models.PostModel;
import awais.instagrabber.models.ViewerPostModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.models.enums.DownloadMethod;
public final class MultiDirectDialog extends BaseLanguageActivity {
public final ArrayList<BasePostModel> selectedItems = new ArrayList<>();
private PostsAdapter postsAdapter;
private MenuItem btnDownload;
private String username = null;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_direct);
final Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
final ViewerPostModel[] postModels;
final Intent intent = getIntent();
if (intent == null || !intent.hasExtra(Constants.EXTRAS_POST)
|| (postModels = (ViewerPostModel[]) intent.getSerializableExtra(Constants.EXTRAS_POST)) == null) {
Utils.errorFinish(this);
return;
}
username = postModels[0].getUsername();
toolbar.setTitle(username);
toolbar.setSubtitle(postModels[0].getShortCode());
final RecyclerView recyclerView = findViewById(R.id.mainPosts);
recyclerView.setNestedScrollingEnabled(false);
recyclerView.setLayoutManager(new GridAutofitLayoutManager(this, Utils.convertDpToPx(130)));
recyclerView.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4)));
final ArrayList<PostModel> models = new ArrayList<>(postModels.length - 1);
for (final ViewerPostModel postModel : postModels)
models.add(new PostModel(postModel.getItemType(), postModel.getPostId(), postModel.getDisplayUrl(),
postModel.getSliderDisplayUrl(), postModel.getShortCode(), postModel.getPostCaption(), postModel.getTimestamp()));
postsAdapter = new PostsAdapter(models, v -> {
final Object tag = v.getTag();
if (tag instanceof PostModel) {
final PostModel postModel = (PostModel) tag;
if (postsAdapter.isSelecting) toggleSelection(postModel);
else {
Utils.batchDownload(this, username, DownloadMethod.DOWNLOAD_DIRECT, Collections.singletonList(postModel));
finish();
}
}
}, v -> {
final Object tag = v.getTag();
if (tag instanceof PostModel) {
postsAdapter.isSelecting = true;
toggleSelection((PostModel) tag);
}
return true;
});
recyclerView.setAdapter(postsAdapter);
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
Utils.batchDownload(this, username, DownloadMethod.DOWNLOAD_DIRECT, selectedItems);
finish();
return true;
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
btnDownload = menu.findItem(R.id.action_download);
menu.findItem(R.id.action_search).setVisible(false);
return true;
}
private void toggleSelection(final PostModel postModel) {
if (postModel != null && postsAdapter != null) {
if (postModel.isSelected()) selectedItems.remove(postModel);
else selectedItems.add(postModel);
postModel.setSelected(!postModel.isSelected());
notifyAdapter(postModel);
}
}
private void notifyAdapter(final PostModel postModel) {
if (selectedItems.size() < 1) postsAdapter.isSelecting = false;
if (postModel.getPosition() < 0) postsAdapter.notifyDataSetChanged();
else postsAdapter.notifyItemChanged(postModel.getPosition(), postModel);
if (btnDownload != null) btnDownload.setVisible(postsAdapter.isSelecting);
}
}

6
app/src/main/java/awais/instagrabber/interfaces/FetchListener.java

@ -0,0 +1,6 @@
package awais.instagrabber.interfaces;
public interface FetchListener<T> {
void onResult(T result);
default void doBefore() { }
}

11
app/src/main/java/awais/instagrabber/interfaces/ItemGetter.java

@ -0,0 +1,11 @@
package awais.instagrabber.interfaces;
import java.io.Serializable;
import java.util.List;
import awais.instagrabber.models.BasePostModel;
import awais.instagrabber.models.enums.ItemGetType;
public interface ItemGetter extends Serializable {
List<? extends BasePostModel> get(final ItemGetType itemGetType);
}

5
app/src/main/java/awais/instagrabber/interfaces/LazyLoadListener.java

@ -0,0 +1,5 @@
package awais.instagrabber.interfaces;
public interface LazyLoadListener {
void onLoadMore(final int page, final int totalItemsCount);
}

7
app/src/main/java/awais/instagrabber/interfaces/MentionClickListener.java

@ -0,0 +1,7 @@
package awais.instagrabber.interfaces;
import awais.instagrabber.customviews.RamboTextView;
public interface MentionClickListener {
void onClick(final RamboTextView view, final String text, final boolean isHashtag);
}

5
app/src/main/java/awais/instagrabber/interfaces/OnGroupClickListener.java

@ -0,0 +1,5 @@
package awais.instagrabber.interfaces;
public interface OnGroupClickListener {
void toggleGroup(final int flatPos);
}

5
app/src/main/java/awais/instagrabber/interfaces/SwipeEvent.java

@ -0,0 +1,5 @@
package awais.instagrabber.interfaces;
public interface SwipeEvent {
void onSwipe(final boolean isRight);
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save