commit 13beabf741f8e62c5d526ef4efdb2d3ddbf1e201 Author: Austin Huang Date: Wed Jul 1 13:08:28 2020 -0400 init diff --git a/.gitignore b/.gitignore new file mode 100755 index 00000000..825ad741 --- /dev/null +++ b/.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 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100755 index 00000000..24e1e984 --- /dev/null +++ b/.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/ diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..0e33933c --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +InstaGrabber \ No newline at end of file diff --git a/.idea/codeStyles b/.idea/codeStyles new file mode 100755 index 00000000..e2e415d5 --- /dev/null +++ b/.idea/codeStyles @@ -0,0 +1,119 @@ + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100755 index 00000000..ac6b0aec --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100755 index 00000000..dd4c951e --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100755 index 00000000..247f71d9 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100755 index 00000000..3ea378e6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml new file mode 100755 index 00000000..8ec256a5 --- /dev/null +++ b/.idea/render.experimental.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100755 index 00000000..7f68460d --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/app.xml b/.idea/runConfigurations/app.xml new file mode 100755 index 00000000..aeebe8e5 --- /dev/null +++ b/.idea/runConfigurations/app.xml @@ -0,0 +1,53 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100755 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG new file mode 100755 index 00000000..6c29fcd6 --- /dev/null +++ b/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 diff --git a/LICENSE b/LICENSE new file mode 100755 index 00000000..3c8b32d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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 . + +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 +. + + 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 +. diff --git a/README.md b/README.md new file mode 100755 index 00000000..66a4ac64 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +![InstaGrabber](./app/src/main/res/mipmap-hdpi/ic_launcher.png "InstaGrabber") InstaGrabber + +Revived Version by Austin. + +Coming soon™ \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100755 index 00000000..796b96d1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100755 index 00000000..d7e73113 --- /dev/null +++ b/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 } +} diff --git a/app/lint.xml b/app/lint.xml new file mode 100755 index 00000000..8fb95bdc --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/play_icon.png b/app/play_icon.png new file mode 100755 index 00000000..b06c141b Binary files /dev/null and b/app/play_icon.png differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100755 index 00000000..f1b42451 --- /dev/null +++ b/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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100755 index 00000000..613d579d --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100755 index 00000000..714fd9cf Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/awais/instagrabber/InstaApp.java b/app/src/main/java/awais/instagrabber/InstaApp.java new file mode 100755 index 00000000..260eeeaf --- /dev/null +++ b/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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/MainHelper.java b/app/src/main/java/awais/instagrabber/MainHelper.java new file mode 100755 index 00000000..20193206 --- /dev/null +++ b/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 prevStoriesFetcher; + private final boolean autoloadPosts; + private boolean hasNextPage = false, feedHasNextPage = false, discoverHasMore = false; + private String endCursor = null, feedEndCursor = null, discoverEndMaxId = null; + private final FetchListener postsFetchListener = new FetchListener() { + @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 feedFetchListener = new FetchListener() { + @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 discoverFetchListener = new FetchListener() { + @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 feedStoriesListener = new FetchListener() { + @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() { + @Override + public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target target, final boolean isFirstResource) { + main.mainBinding.mainProfileImage.setEnabled(false); + return false; + } + + @Override + public boolean onResourceReady(final Drawable resource, final Object model, final Target 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(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.java b/app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.java new file mode 100755 index 00000000..781aa5ea --- /dev/null +++ b/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); + } +} diff --git a/app/src/main/java/awais/instagrabber/activities/CommentsViewer.java b/app/src/main/java/awais/instagrabber/activities/CommentsViewer.java new file mode 100755 index 00000000..1c2c4438 --- /dev/null +++ b/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 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() { + @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; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/DirectMessages.java b/app/src/main/java/awais/instagrabber/activities/DirectMessages.java new file mode 100755 index 00000000..6449cdc8 --- /dev/null +++ b/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 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 fetchListener = new FetchListener() { + @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 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); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/DirectMessagesUserInbox.java b/app/src/main/java/awais/instagrabber/activities/DirectMessagesUserInbox.java new file mode 100755 index 00000000..e8b077d5 --- /dev/null +++ b/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 users = new ArrayList<>(); + private final ArrayList directItemModels = new ArrayList<>(); + private final FetchListener fetchListener = new FetchListener() { + @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 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/FollowViewer.java b/app/src/main/java/awais/instagrabber/activities/FollowViewer.java new file mode 100755 index 00000000..e1dceacb --- /dev/null +++ b/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 followModels = new ArrayList<>(); + private final ArrayList followingModels = new ArrayList<>(); + private final ArrayList followersModels = new ArrayList<>(); + private final ArrayList 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 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 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 fetchListener = new FetchListener() { + @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 followingFetchListener = new FetchListener() { + @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 followersFetchListener = new FetchListener() { + @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 searchFollowModels = new ArrayList<>(followModels.size() / 2); +// private final ArrayList searchFollowingModels = new ArrayList<>(followingModels.size() / 2); +// private final ArrayList searchFollowersModels = new ArrayList<>(followersModels.size() / 2); +// private final ArrayList 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 followModels, final ArrayList followingModels, + final ArrayList followersModels, final ArrayList allFollowing) { + final ArrayList 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); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/Login.java b/app/src/main/java/awais/instagrabber/activities/Login.java new file mode 100755 index 00000000..db825719 --- /dev/null +++ b/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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/Main.java b/app/src/main/java/awais/instagrabber/activities/Main.java new file mode 100755 index 00000000..188abc6d --- /dev/null +++ b/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 scanHack; + public static ItemGetter itemGetter; + // -------- items -------- + public final ArrayList allItems = new ArrayList<>(); + public final ArrayList feedItems = new ArrayList<>(); + public final ArrayList discoverItems = new ArrayList<>(); + // -------- items -------- + public final ArrayList selectedItems = new ArrayList<>(); + public final ArrayList 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 profileDialogAdapter; + private DialogInterface.OnClickListener profileDialogListener; + private Stack 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 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 fetchListener = new FetchListener() { + @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) 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/PostViewer.java b/app/src/main/java/awais/instagrabber/activities/PostViewer.java new file mode 100755 index 00000000..21254a79 --- /dev/null +++ b/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 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 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() { + @Override + public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target target, final boolean isFirstResource) { + viewerBinding.progressView.setVisibility(View.GONE); + return false; + } + + @Override + public boolean onResourceReady(final Drawable resource, final Object model, final Target target, final DataSource dataSource, final boolean isFirstResource) { + viewerBinding.progressView.setVisibility(View.GONE); + return false; + } + }).into(viewerBinding.imageViewer); + } + + private void showDownloadDialog() { + final ArrayList 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() { + private boolean loaded = true; + + @Override + public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target 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 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/ProfileViewer.java b/app/src/main/java/awais/instagrabber/activities/ProfileViewer.java new file mode 100755 index 00000000..c6bc5a80 --- /dev/null +++ b/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 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() { + @Override + public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target 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 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/StoryViewer.java b/app/src/main/java/awais/instagrabber/activities/StoryViewer.java new file mode 100755 index 00000000..01cf541e --- /dev/null +++ b/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() { + @Override + public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target target, final boolean isFirstResource) { + storyViewerBinding.progressView.setVisibility(View.GONE); + return false; + } + + @Override + public boolean onResourceReady(final Drawable resource, final Object model, final Target 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; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java new file mode 100755 index 00000000..69221bf0 --- /dev/null +++ b/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 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 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectMessagesAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectMessagesAdapter.java new file mode 100755 index 00000000..0c8717ab --- /dev/null +++ b/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 { + private final ArrayList inboxThreadModels; + private final View.OnClickListener onClickListener; + private LayoutInflater layoutInflater; + + public DirectMessagesAdapter(final ArrayList 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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/DiscoverAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DiscoverAdapter.java new file mode 100755 index 00000000..ebb0231c --- /dev/null +++ b/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 { + private final ArrayList discoverItemModels; + private final View.OnClickListener clickListener; + private final View.OnLongClickListener longClickListener; + private LayoutInflater layoutInflater; + public boolean isSelecting = false; + + public DiscoverAdapter(final ArrayList 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() { + @Override + public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target target, final boolean isFirstResource) { + holder.progressView.setVisibility(View.GONE); + return false; + } + + @Override + public boolean onResourceReady(final Drawable resource, final Object model, final Target 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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java b/app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java new file mode 100755 index 00000000..98d8d8bb --- /dev/null +++ b/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 { + private final static String ellipsize = "… more"; + private final Activity activity; + private final LayoutInflater layoutInflater; + private final ArrayList 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 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 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; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java b/app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java new file mode 100755 index 00000000..f28bfeb3 --- /dev/null +++ b/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 { + 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; + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java b/app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java new file mode 100755 index 00000000..7466022b --- /dev/null +++ b/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 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 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 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)]; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/HighlightsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/HighlightsAdapter.java new file mode 100755 index 00000000..af7460f7 --- /dev/null +++ b/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 { + 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; + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/MessageItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/MessageItemsAdapter.java new file mode 100755 index 00000000..043e15c4 --- /dev/null +++ b/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 { + 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 directItemModels; + private final ArrayList 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 directItemModels, final ArrayList 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/PostsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/PostsAdapter.java new file mode 100755 index 00000000..4fb4598f --- /dev/null +++ b/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 { + private final ArrayList postModels; + private final View.OnClickListener clickListener; + private final View.OnLongClickListener longClickListener; + private LayoutInflater layoutInflater; + public boolean isSelecting = false; + + public PostsAdapter(final ArrayList 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() { + @Override + public boolean onResourceReady(final Drawable resource, final Object model, final Target 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 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(); + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/PostsMediaAdapter.java b/app/src/main/java/awais/instagrabber/adapters/PostsMediaAdapter.java new file mode 100755 index 00000000..f2786b6b --- /dev/null +++ b/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 { + 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/SimpleAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SimpleAdapter.java new file mode 100755 index 00000000..bfdbd7b5 --- /dev/null +++ b/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 extends RecyclerView.Adapter { + private List items; + private final LayoutInflater layoutInflater; + private final View.OnClickListener onClickListener; + private final View.OnLongClickListener longClickListener; + + public SimpleAdapter(final Context context, final List items, final View.OnClickListener onClickListener) { + this(context, items, onClickListener, null); + } + + public SimpleAdapter(final Context context, final List 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 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); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/StoriesAdapter.java b/app/src/main/java/awais/instagrabber/adapters/StoriesAdapter.java new file mode 100755 index 00000000..411fb98e --- /dev/null +++ b/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 { + 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); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java new file mode 100755 index 00000000..601bc639 --- /dev/null +++ b/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)); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java new file mode 100755 index 00000000..08476443 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/DirectMessageViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/DirectMessageViewHolder.java new file mode 100755 index 00000000..c7cdddb4 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/DiscoverViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/DiscoverViewHolder.java new file mode 100755 index 00000000..6795ceb6 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedItemViewHolder.java new file mode 100755 index 00000000..96aafd11 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java new file mode 100755 index 00000000..fb52eac1 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/HighlightViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/HighlightViewHolder.java new file mode 100755 index 00000000..1d1654ff --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/PostMediaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/PostMediaViewHolder.java new file mode 100755 index 00000000..65d115bd --- /dev/null +++ b/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); + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewHolder.java new file mode 100755 index 00000000..f76778b8 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/TextMessageViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/TextMessageViewHolder.java new file mode 100755 index 00000000..3e1bed67 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java new file mode 100755 index 00000000..2ac012ba --- /dev/null +++ b/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 { + private final String shortCode; + private final FetchListener 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 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 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 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 getParentComments() { + final ArrayList 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; + } +} diff --git a/app/src/main/java/awais/instagrabber/asyncs/DiscoverFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/DiscoverFetcher.java new file mode 100755 index 00000000..aa2a9950 --- /dev/null +++ b/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 { + private final String maxId; + private final FetchListener fetchListener; + private int lastId = 0; + private boolean isFirst, moreAvailable; + private String nextMaxId; + + public DiscoverFetcher(final String maxId, final FetchListener 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 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 fetchItems(final File downloadDir, final File customDir, + ArrayList 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/DownloadAsync.java b/app/src/main/java/awais/instagrabber/asyncs/DownloadAsync.java new file mode 100755 index 00000000..57c2d813 --- /dev/null +++ b/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 { + private static int lastNotifId = 1; + private final int currentNotifId; + private final AtomicReference context; + private final File outFile; + private final String url; + private final FetchListener 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 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/FeedFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/FeedFetcher.java new file mode 100755 index 00000000..40d3a9de --- /dev/null +++ b/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 { + 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 fetchListener; + + public FeedFetcher(final FetchListener fetchListener) { + this.endCursor = ""; + this.fetchListener = fetchListener; + } + + public FeedFetcher(final String endCursor, final FetchListener 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":"","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 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/FeedStoriesFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/FeedStoriesFetcher.java new file mode 100755 index 00000000..8084eab9 --- /dev/null +++ b/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 { + private final FetchListener fetchListener; + + public FeedStoriesFetcher(final FetchListener 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/FollowFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/FollowFetcher.java new file mode 100755 index 00000000..103657d1 --- /dev/null +++ b/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 { + private final String endCursor, id; + private final boolean isFollowers; + private final FetchListener fetchListener; + + public FollowFetcher(final String id, final boolean isFollowers, final FetchListener 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 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/HighlightsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/HighlightsFetcher.java new file mode 100755 index 00000000..b2e756e8 --- /dev/null +++ b/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 { + private final String id; + private final FetchListener fetchListener; + + public HighlightsFetcher(final String id, final FetchListener 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java new file mode 100755 index 00000000..9fe076f4 --- /dev/null +++ b/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 { + private final String shortCode; + private final FetchListener fetchListener; + + public PostFetcher(final String shortCode, final FetchListener 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); + } +} diff --git a/app/src/main/java/awais/instagrabber/asyncs/PostsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/PostsFetcher.java new file mode 100755 index 00000000..42b2ce83 --- /dev/null +++ b/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 { + private final String endCursor; + private final String id; + private final FetchListener fetchListener; + private String username; + + public PostsFetcher(final String id, final FetchListener fetchListener) { + this.id = id; + this.endCursor = ""; + this.fetchListener = fetchListener; + } + + public PostsFetcher(final String id, final String endCursor, final FetchListener 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); + } +} diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java new file mode 100755 index 00000000..c24735b8 --- /dev/null +++ b/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 { + private final FetchListener fetchListener; + private final String userName; + + public ProfileFetcher(String userName, FetchListener 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); + } +} diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfilePictureFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/ProfilePictureFetcher.java new file mode 100755 index 00000000..a41583b1 --- /dev/null +++ b/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 { + private final FetchListener fetchListener; + private final String userName, userId; + private final ProfilePictureFetchMode fetchMode; + + public ProfilePictureFetcher(final String userName, final String userId, final FetchListener 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/StoryStatusFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/StoryStatusFetcher.java new file mode 100755 index 00000000..fbf9030d --- /dev/null +++ b/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 { + private final String id; + private final FetchListener fetchListener; + + public StoryStatusFetcher(final String id, final FetchListener 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/SuggestionsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/SuggestionsFetcher.java new file mode 100755 index 00000000..4a599f1e --- /dev/null +++ b/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 { + private final FetchListener fetchListener; + + public SuggestionsFetcher(final FetchListener 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 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/UsernameFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/UsernameFetcher.java new file mode 100755 index 00000000..0efff6b9 --- /dev/null +++ b/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 { + private final FetchListener fetchListener; + private final String uid; + + public UsernameFetcher(final String uid, final FetchListener 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); + } +} diff --git a/app/src/main/java/awais/instagrabber/asyncs/direct_messages/InboxFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/direct_messages/InboxFetcher.java new file mode 100755 index 00000000..0d185bcf --- /dev/null +++ b/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 { + private final String endCursor; + private final FetchListener fetchListener; + + public InboxFetcher(final String endCursor, final FetchListener 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/direct_messages/UserInboxFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/direct_messages/UserInboxFetcher.java new file mode 100755 index 00000000..8134aab2 --- /dev/null +++ b/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 { + private final String id; + private final String endCursor; + private final FetchListener fetchListener; + private final String direction; + + public UserInboxFetcher(final String id, final UserInboxDirection direction, final String endCursor, + final FetchListener 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java b/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java new file mode 100755 index 00000000..4ebffb6b --- /dev/null +++ b/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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/CommentMentionClickSpan.java b/app/src/main/java/awais/instagrabber/customviews/CommentMentionClickSpan.java new file mode 100755 index 00000000..a68509c1 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/FixedImageView.java b/app/src/main/java/awais/instagrabber/customviews/FixedImageView.java new file mode 100755 index 00000000..302b1f94 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/MouseDrawer.java b/app/src/main/java/awais/instagrabber/customviews/MouseDrawer.java new file mode 100755 index 00000000..8491be2e --- /dev/null +++ b/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 mNonDrawerViews = new ArrayList<>(); + private final ViewDragHelper mLeftDragger, mRightDragger; + private boolean mInLayout, mFirstLayout = true; + private float mDrawerElevation, mInitialMotionX, mInitialMotionY; + private int mDrawerState; + private List 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 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 CREATOR = new ClassLoaderCreator() { + @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); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/RamboTextView.java b/app/src/main/java/awais/instagrabber/customviews/RamboTextView.java new file mode 100755 index 00000000..93ea7cf0 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/RemixDrawerLayout.java b/app/src/main/java/awais/instagrabber/customviews/RemixDrawerLayout.java new file mode 100755 index 00000000..e41ac94d --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/GridAutofitLayoutManager.java b/app/src/main/java/awais/instagrabber/customviews/helpers/GridAutofitLayoutManager.java new file mode 100755 index 00000000..d10d96b8 --- /dev/null +++ b/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); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java b/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java new file mode 100755 index 00000000..c0f24d21 --- /dev/null +++ b/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; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoader.java b/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoader.java new file mode 100755 index 00000000..b71eed0b --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/SwipeGestureListener.java b/app/src/main/java/awais/instagrabber/customviews/helpers/SwipeGestureListener.java new file mode 100755 index 00000000..fcd3f809 --- /dev/null +++ b/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; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java b/app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java new file mode 100755 index 00000000..1deff1ad --- /dev/null +++ b/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 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 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/SoundParser.java b/app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/SoundParser.java new file mode 100755 index 00000000..6fb309d5 --- /dev/null +++ b/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 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 extensions) { + additionalExtensions.addAll(extensions); + } + + static void removeCustomExtensions(final List 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveFormProgressChangeListener.java b/app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveFormProgressChangeListener.java new file mode 100755 index 00000000..326bc102 --- /dev/null +++ b/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); +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveGravity.java b/app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveGravity.java new file mode 100755 index 00000000..5ac68de0 --- /dev/null +++ b/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, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveformSeekBar.java b/app/src/main/java/awais/instagrabber/customviews/masoudss_waveform/WaveformSeekBar.java new file mode 100755 index 00000000..6132a6e4 --- /dev/null +++ b/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(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/dialogs/AboutDialog.java b/app/src/main/java/awais/instagrabber/dialogs/AboutDialog.java new file mode 100755 index 00000000..f727b885 --- /dev/null +++ b/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:chapter50000@hotmail.com"), 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/dialogs/ProfileSettingsDialog.java b/app/src/main/java/awais/instagrabber/dialogs/ProfileSettingsDialog.java new file mode 100755 index 00000000..909227c3 --- /dev/null +++ b/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) { } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/dialogs/QuickAccessDialog.java b/app/src/main/java/awais/instagrabber/dialogs/QuickAccessDialog.java new file mode 100755 index 00000000..ddae5a62 --- /dev/null +++ b/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 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 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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/dialogs/SettingsDialog.java b/app/src/main/java/awais/instagrabber/dialogs/SettingsDialog.java new file mode 100755 index 00000000..5b116cd8 --- /dev/null +++ b/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) { } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/dialogs/TimeSettingsDialog.java b/app/src/main/java/awais/instagrabber/dialogs/TimeSettingsDialog.java new file mode 100755 index 00000000..f9225d9f --- /dev/null +++ b/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) { } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/directdownload/DirectDownload.java b/app/src/main/java/awais/instagrabber/directdownload/DirectDownload.java new file mode 100755 index 00000000..005fda27 --- /dev/null +++ b/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() { + @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); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/directdownload/MultiDirectDialog.java b/app/src/main/java/awais/instagrabber/directdownload/MultiDirectDialog.java new file mode 100755 index 00000000..86bb7150 --- /dev/null +++ b/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 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 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/interfaces/FetchListener.java b/app/src/main/java/awais/instagrabber/interfaces/FetchListener.java new file mode 100755 index 00000000..e9b43f00 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/interfaces/FetchListener.java @@ -0,0 +1,6 @@ +package awais.instagrabber.interfaces; + +public interface FetchListener { + void onResult(T result); + default void doBefore() { } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/interfaces/ItemGetter.java b/app/src/main/java/awais/instagrabber/interfaces/ItemGetter.java new file mode 100755 index 00000000..c53c2491 --- /dev/null +++ b/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 get(final ItemGetType itemGetType); +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/interfaces/LazyLoadListener.java b/app/src/main/java/awais/instagrabber/interfaces/LazyLoadListener.java new file mode 100755 index 00000000..ca98a3f1 --- /dev/null +++ b/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); +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/interfaces/MentionClickListener.java b/app/src/main/java/awais/instagrabber/interfaces/MentionClickListener.java new file mode 100755 index 00000000..d8d0a347 --- /dev/null +++ b/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); +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/interfaces/OnGroupClickListener.java b/app/src/main/java/awais/instagrabber/interfaces/OnGroupClickListener.java new file mode 100755 index 00000000..d7763851 --- /dev/null +++ b/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); +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/interfaces/SwipeEvent.java b/app/src/main/java/awais/instagrabber/interfaces/SwipeEvent.java new file mode 100755 index 00000000..89eba321 --- /dev/null +++ b/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); +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/BasePostModel.java b/app/src/main/java/awais/instagrabber/models/BasePostModel.java new file mode 100755 index 00000000..7a0dca86 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/BasePostModel.java @@ -0,0 +1,82 @@ +package awais.instagrabber.models; + +import androidx.annotation.NonNull; + +import java.io.Serializable; +import java.util.Date; + +import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.utils.Utils; + +public abstract class BasePostModel implements Serializable { + protected String postId; + protected String displayUrl; + protected String shortCode; + protected CharSequence postCaption; + protected MediaItemType itemType; + protected boolean isSelected; + protected boolean isDownloaded; + protected long timestamp; + protected int position; + + public MediaItemType getItemType() { + return itemType; + } + + public final String getPostId() { + return postId; + } + + public final String getDisplayUrl() { + return displayUrl; + } + + public final CharSequence getPostCaption() { + return postCaption; + } + + public final String getShortCode() { + return shortCode; + } + + public final long getTimestamp() { + return timestamp; + } + + public int getPosition() { + return this.position; + } + + public boolean isSelected() { + return isSelected; + } + + public boolean isDownloaded() { + return isDownloaded; + } + + public void setItemType(final MediaItemType itemType) { + this.itemType = itemType; + } + + public void setPostId(final String postId) { + this.postId = postId; + } + + public void setPosition(final int position) { + this.position = position; + } + + public void setSelected(final boolean selected) { + this.isSelected = selected; + } + + public void setDownloaded(final boolean downloaded) { + isDownloaded = downloaded; + } + + @NonNull + public final String getPostDate() { + return Utils.datetimeParser.format(new Date(timestamp * 1000L)); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/CommentModel.java b/app/src/main/java/awais/instagrabber/models/CommentModel.java new file mode 100755 index 00000000..130139aa --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/CommentModel.java @@ -0,0 +1,85 @@ +package awais.instagrabber.models; + +import androidx.annotation.NonNull; + +import java.util.Date; + +import awais.instagrabber.utils.Utils; + +public final class CommentModel { + private final ProfileModel profileModel; + private final String id; + private final CharSequence text; + private final long likes, timestamp; + private CommentModel[] childCommentModels; + private boolean hasNextPage; + private String endCursor; + + public CommentModel(final String id, final String text, final long timestamp, final long likes, final ProfileModel profileModel) { + this.id = id; + this.text = Utils.hasMentions(text) ? Utils.getMentionText(text) : text; + this.likes = likes; + this.timestamp = timestamp; + this.profileModel = profileModel; + } + + public String getId() { + return id; + } + + public CharSequence getText() { + return text; + } + + @NonNull + public String getDateTime() { + return Utils.datetimeParser.format(new Date(timestamp * 1000L)); + } + + public long getLikes() { + return likes; + } + + public ProfileModel getProfileModel() { + return profileModel; + } + + public CommentModel[] getChildCommentModels() { + return childCommentModels; + } + + public void setChildCommentModels(final CommentModel[] childCommentModels) { + this.childCommentModels = childCommentModels; + } + + public void setPageCursor(final boolean hasNextPage, final String endCursor) { + this.hasNextPage = hasNextPage; + this.endCursor = endCursor; + } + + public boolean hasNextPage() { + return hasNextPage; + } + + public String getEndCursor() { + return endCursor; + } + +// @NonNull +// @Override +// public String toString() { +// try { +// final JSONObject object = new JSONObject(); +// object.put(Constants.EXTRAS_ID, id); +// object.put("text", text); +// object.put(Constants.EXTRAS_NAME, profileModel != null ? profileModel.getUsername() : ""); +// if (childCommentModels != null) object.put("childComments", childCommentModels); +// return object.toString(); +// } catch (Exception e) { +// return "{\"id\":\"" + id + "\", \"text\":\"" + text +// //(text != null ? text.replaceAll("\"", "\\\\\"") : "") +// + "\", \"name\":\"" + (profileModel != null ? profileModel.getUsername() : "") + +// (childCommentModels != null ? "\", \"childComments\":" + childCommentModels.length : "\"") + '}'; +// } +// } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/DiscoverItemModel.java b/app/src/main/java/awais/instagrabber/models/DiscoverItemModel.java new file mode 100755 index 00000000..925763b8 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/DiscoverItemModel.java @@ -0,0 +1,29 @@ +package awais.instagrabber.models; + +import awais.instagrabber.models.enums.MediaItemType; + +public final class DiscoverItemModel extends BasePostModel { + private boolean moreAvailable; + private String nextMaxId; + + public DiscoverItemModel(final MediaItemType mediaType, final String postId, final String shortCode, final String thumbnail) { + this.postId = postId; + this.itemType = mediaType; + this.shortCode = shortCode; + this.displayUrl = thumbnail; + } + + + public void setMore(final boolean moreAvailable, final String nextMaxId) { + this.moreAvailable = moreAvailable; + this.nextMaxId = nextMaxId; + } + + public boolean hasMore() { + return moreAvailable; + } + + public String getNextMaxId() { + return nextMaxId; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/FeedModel.java b/app/src/main/java/awais/instagrabber/models/FeedModel.java new file mode 100755 index 00000000..b010a8d2 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/FeedModel.java @@ -0,0 +1,56 @@ +package awais.instagrabber.models; + +import awais.instagrabber.models.enums.MediaItemType; + +public final class FeedModel extends PostModel { + private final ProfileModel profileModel; + private final long commentsCount, viewCount; + private boolean captionExpanded = false, mentionClicked = false; + private ViewerPostModel[] sliderItems; + + public FeedModel(final ProfileModel profileModel, final MediaItemType itemType, final long viewCount, final String postId, + final String displayUrl, final String thumbnailUrl, final String shortCode, final String postCaption, + final long commentsCount, final long timestamp) { + super(itemType, postId, displayUrl, thumbnailUrl, shortCode, postCaption, timestamp); + this.profileModel = profileModel; + this.commentsCount = commentsCount; + this.viewCount = viewCount; + } + + public ProfileModel getProfileModel() { + return profileModel; + } + + public ViewerPostModel[] getSliderItems() { + return sliderItems; + } + + public long getViewCount() { + return viewCount; + } + + public long getCommentsCount() { + return commentsCount; + } + + public boolean isCaptionExpanded() { + return captionExpanded; + } + + public boolean isMentionClicked() { + return !mentionClicked; + } + + public void setMentionClicked(final boolean mentionClicked) { + this.mentionClicked = mentionClicked; + } + + public void setSliderItems(final ViewerPostModel[] sliderItems) { + this.sliderItems = sliderItems; + setItemType(MediaItemType.MEDIA_TYPE_SLIDER); + } + + public void toggleCaption() { + captionExpanded = !captionExpanded; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java b/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java new file mode 100755 index 00000000..6e6e7362 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java @@ -0,0 +1,30 @@ +package awais.instagrabber.models; + +import java.io.Serializable; + +public final class FeedStoryModel implements Serializable { + private final String storyMediaId; + private final ProfileModel profileModel; + private StoryModel[] storyModels; + + public FeedStoryModel(final String storyMediaId, final ProfileModel profileModel) { + this.storyMediaId = storyMediaId; + this.profileModel = profileModel; + } + + public String getStoryMediaId() { + return storyMediaId; + } + + public ProfileModel getProfileModel() { + return profileModel; + } + + public void setStoryModels(final StoryModel[] storyModels) { + this.storyModels = storyModels; + } + + public StoryModel[] getStoryModels() { + return storyModels; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/FollowModel.java b/app/src/main/java/awais/instagrabber/models/FollowModel.java new file mode 100755 index 00000000..fd9cc7a4 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/FollowModel.java @@ -0,0 +1,64 @@ +package awais.instagrabber.models; + +import androidx.annotation.Nullable; + +import java.io.Serializable; + +public final class FollowModel implements Serializable { + private final String id, username, fullName, profilePicUrl; + private String endCursor; + private boolean hasNextPage, isShown = true; + + public FollowModel(final String id, final String username, final String fullName, final String profilePicUrl) { + this.id = id; + this.username = username; + this.fullName = fullName; + this.profilePicUrl = profilePicUrl; + } + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getFullName() { + return fullName; + } + + public String getProfilePicUrl() { + return profilePicUrl; + } + + public boolean isShown() { + return isShown; + } + + public void setShown(final boolean shown) { + isShown = shown; + } + + public void setPageCursor(final boolean hasNextPage, final String endCursor) { + this.endCursor = endCursor; + this.hasNextPage = hasNextPage; + } + + public boolean hasNextPage() { + return endCursor != null && hasNextPage; + } + + public String getEndCursor() { + return endCursor; + } + + @Override + public boolean equals(@Nullable final Object obj) { + if (obj instanceof FollowModel) { + final FollowModel model = (FollowModel) obj; + if (model.getId().equals(id) && model.getUsername().equals(username)) return true; + } + return super.equals(obj); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/HighlightModel.java b/app/src/main/java/awais/instagrabber/models/HighlightModel.java new file mode 100755 index 00000000..d7af017a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/HighlightModel.java @@ -0,0 +1,27 @@ +package awais.instagrabber.models; + +public final class HighlightModel { + private final String title, thumbnailUrl; + private StoryModel[] storyModels; + + public HighlightModel(final String title, final String thumbnailUrl) { + this.title = title; + this.thumbnailUrl = thumbnailUrl; + } + + public String getTitle() { + return title; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public StoryModel[] getStoryModels() { + return storyModels; + } + + public void setStoryModels(final StoryModel[] storyModels) { + this.storyModels = storyModels; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/IntentModel.java b/app/src/main/java/awais/instagrabber/models/IntentModel.java new file mode 100755 index 00000000..3348bb2e --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/IntentModel.java @@ -0,0 +1,21 @@ +package awais.instagrabber.models; + +import awais.instagrabber.models.enums.IntentModelType; + +public final class IntentModel { + private final IntentModelType type; + private final String text; + + public IntentModel(final IntentModelType type, final String text) { + this.type = type; + this.text = text; + } + + public IntentModelType getType() { + return type; + } + + public String getText() { + return text; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/PostModel.java b/app/src/main/java/awais/instagrabber/models/PostModel.java new file mode 100755 index 00000000..ad8550c3 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/PostModel.java @@ -0,0 +1,50 @@ +package awais.instagrabber.models; + +import awais.instagrabber.models.enums.MediaItemType; + +public class PostModel extends BasePostModel { + protected final String thumbnailUrl; + protected String endCursor; + protected boolean hasNextPage; + + public PostModel(final String shortCode) { + this.shortCode = shortCode; + this.thumbnailUrl = null; + } + + public PostModel(final MediaItemType itemType, final String postId, final String displayUrl, final String thumbnailUrl, + final String shortCode, final CharSequence postCaption, long timestamp) { + this.itemType = itemType; + this.postId = postId; + this.displayUrl = displayUrl; + this.thumbnailUrl = thumbnailUrl; + this.shortCode = shortCode; + this.postCaption = postCaption; + this.timestamp = timestamp; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public String getEndCursor() { + return endCursor; + } + + public boolean hasNextPage() { + return endCursor != null && hasNextPage; + } + + public void setPostCaption(final CharSequence postCaption) { + this.postCaption = postCaption; + } + + public void setTimestamp(final long timestamp) { + this.timestamp = timestamp; + } + + public void setPageCursor(final boolean hasNextPage, final String endCursor) { + this.endCursor = endCursor; + this.hasNextPage = hasNextPage; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/ProfileModel.java b/app/src/main/java/awais/instagrabber/models/ProfileModel.java new file mode 100755 index 00000000..43989209 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/ProfileModel.java @@ -0,0 +1,75 @@ +package awais.instagrabber.models; + +import java.io.Serializable; + +public final class ProfileModel implements Serializable { + private final boolean isPrivate, isVerified; + private final long postCount, followersCount, followingCount; + private final String id, username, name, biography, url, sdProfilePic, hdProfilePic; + + public ProfileModel(final boolean isPrivate, final boolean isVerified, final String id, final String username, + final String name, final String biography, final String url, final String sdProfilePic, final String hdProfilePic, + final long postCount, final long followersCount, final long followingCount) { + this.isPrivate = isPrivate; + this.isVerified = isVerified; + this.id = id; + this.url = url; + this.name = name; + this.username = username; + this.biography = biography; + this.sdProfilePic = sdProfilePic; + this.hdProfilePic = hdProfilePic; + this.postCount = postCount; + this.followersCount = followersCount; + this.followingCount = followingCount; + } + + public boolean isPrivate() { + return isPrivate; + } + + public boolean isVerified() { + return isVerified; + } + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getName() { + return name; + } + + public String getBiography() { + return biography; + } + + public String getUrl() { + return url; + } + + public String getSdProfilePic() { + return sdProfilePic; + } + + public String getHdProfilePic() { + return hdProfilePic; + } + + public long getPostCount() { + return postCount; + } + + public long getFollowersCount() { + return followersCount; + } + + public long getFollowingCount() { + return followingCount; + } + +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/StoryModel.java b/app/src/main/java/awais/instagrabber/models/StoryModel.java new file mode 100755 index 00000000..9ebed24f --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/StoryModel.java @@ -0,0 +1,69 @@ +package awais.instagrabber.models; + +import java.io.Serializable; + +import awais.instagrabber.models.enums.MediaItemType; + +public final class StoryModel implements Serializable { + private final String storyMediaId, storyUrl; + private final MediaItemType itemType; + private final long timestamp; + private String videoUrl, tappableShortCode; + private int position; + private boolean isCurrentSlide = false; + + public StoryModel(final String storyMediaId, final String storyUrl, final MediaItemType itemType, final long timestamp) { + this.storyMediaId = storyMediaId; + this.storyUrl = storyUrl; + this.itemType = itemType; + this.timestamp = timestamp; + } + + public String getStoryUrl() { + return storyUrl; + } + + public String getStoryMediaId() { + return storyMediaId; + } + + public MediaItemType getItemType() { + return itemType; + } + + public long getTimestamp() { + return timestamp; + } + + public String getVideoUrl() { + return videoUrl; + } + + public String getTappableShortCode() { + return tappableShortCode; + } + + public int getPosition() { + return position; + } + + public void setVideoUrl(final String videoUrl) { + this.videoUrl = videoUrl; + } + + public void setTappableShortCode(final String tappableShortCode) { + this.tappableShortCode = tappableShortCode; + } + + public void setPosition(final int position) { + this.position = position; + } + + public void setCurrentSlide(final boolean currentSlide) { + this.isCurrentSlide = currentSlide; + } + + public boolean isCurrentSlide() { + return isCurrentSlide; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/SuggestionModel.java b/app/src/main/java/awais/instagrabber/models/SuggestionModel.java new file mode 100755 index 00000000..855b1d4b --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/SuggestionModel.java @@ -0,0 +1,51 @@ +package awais.instagrabber.models; + +import androidx.annotation.NonNull; + +import awais.instagrabber.models.enums.SuggestionType; + +public final class SuggestionModel implements Comparable { + private final int position; + private final boolean isVerified; + private final String username, name, profilePic; + private final SuggestionType suggestionType; + + public SuggestionModel(final boolean isVerified, final String username, final String name, final String profilePic, + final SuggestionType suggestionType, final int position) { + this.isVerified = isVerified; + this.username = username; + this.name = name; + this.profilePic = profilePic; + this.suggestionType = suggestionType; + this.position = position; + } + + public boolean isVerified() { + return isVerified; + } + + public String getUsername() { + return username; + } + + public String getName() { + return name; + } + + public String getProfilePic() { + return profilePic; + } + + public SuggestionType getSuggestionType() { + return suggestionType; + } + + public int getPosition() { + return position; + } + + @Override + public int compareTo(@NonNull final SuggestionModel model) { + return Integer.compare(getPosition(), model.getPosition()); + } +} diff --git a/app/src/main/java/awais/instagrabber/models/ViewerPostModel.java b/app/src/main/java/awais/instagrabber/models/ViewerPostModel.java new file mode 100755 index 00000000..13271006 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/ViewerPostModel.java @@ -0,0 +1,63 @@ +package awais.instagrabber.models; + +import awais.instagrabber.models.enums.MediaItemType; + +public final class ViewerPostModel extends BasePostModel { + protected final String username; + protected final long videoViews; + protected String sliderDisplayUrl, commentsEndCursor; + protected long commentsCount; + private boolean isCurrentSlide = false; + + public ViewerPostModel(final MediaItemType itemType, final String postId, final String displayUrl, final String shortCode, + final String postCaption, final String username, final long videoViews, final long timestamp) { + this.itemType = itemType; + this.postId = postId; + this.displayUrl = displayUrl; + this.postCaption = postCaption; + this.username = username; + this.shortCode = shortCode; + this.videoViews = videoViews; + this.timestamp = timestamp; + } + + public long getCommentsCount() { + return commentsCount; + } + + public String getSliderDisplayUrl() { + return sliderDisplayUrl; + } + + public String getUsername() { + return username; + } + + public String getCommentsEndCursor() { + return commentsEndCursor; + } + + public final long getVideoViews() { + return videoViews; + } + + public void setSliderDisplayUrl(final String sliderDisplayUrl) { + this.sliderDisplayUrl = sliderDisplayUrl; + } + + public void setCommentsCount(final long commentsCount) { + this.commentsCount = commentsCount; + } + + public void setCommentsEndCursor(final String commentsEndCursor) { + this.commentsEndCursor = commentsEndCursor; + } + + public void setCurrentSlide(final boolean currentSlide) { + this.isCurrentSlide = currentSlide; + } + + public boolean isCurrentSlide() { + return isCurrentSlide; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/direct_messages/DirectItemModel.java b/app/src/main/java/awais/instagrabber/models/direct_messages/DirectItemModel.java new file mode 100755 index 00000000..87e82d8b --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/direct_messages/DirectItemModel.java @@ -0,0 +1,490 @@ +package awais.instagrabber.models.direct_messages; + +import androidx.annotation.NonNull; + +import java.io.Serializable; +import java.util.Date; + +import awais.instagrabber.models.ProfileModel; +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; + +public final class DirectItemModel implements Serializable, Comparable { + private final long userId, timestamp; + private final DirectItemType itemType; + private final String itemId; + private final CharSequence text; + private final DirectItemLinkModel linkModel; + private final DirectItemMediaModel mediaModel; + private final ProfileModel profileModel; + private final DirectItemReelShareModel reelShare; + private final DirectItemActionLogModel actionLogModel; + private final DirectItemVoiceMediaModel voiceMediaModel; + private final DirectItemRavenMediaModel ravenMediaModel; + private final DirectItemAnimatedMediaModel animatedMediaModel; + private final DirectItemVideoCallEventModel videoCallEventModel; + + public DirectItemModel(final long userId, final long timestamp, final String itemId, final DirectItemType itemType, + final CharSequence text, final DirectItemLinkModel linkModel, final ProfileModel profileModel, + final DirectItemReelShareModel reelShare, final DirectItemMediaModel mediaModel, + final DirectItemActionLogModel actionLogModel, final DirectItemVoiceMediaModel voiceMediaModel, + final DirectItemRavenMediaModel ravenMediaModel, final DirectItemVideoCallEventModel videoCallEventModel, + final DirectItemAnimatedMediaModel animatedMediaModel) { + this.userId = userId; + this.timestamp = timestamp; + this.itemType = itemType; + this.itemId = itemId; + this.text = text; + this.linkModel = linkModel; + this.profileModel = profileModel; + this.reelShare = reelShare; + this.mediaModel = mediaModel; + this.actionLogModel = actionLogModel; + this.voiceMediaModel = voiceMediaModel; + this.ravenMediaModel = ravenMediaModel; + this.videoCallEventModel = videoCallEventModel; + this.animatedMediaModel = animatedMediaModel; + } + + public DirectItemType getItemType() { + return itemType; + } + + public CharSequence getText() { + return text; + } + + public String getItemId() { + return itemId; + } + + public long getUserId() { + return userId; + } + + public long getTimestamp() { + return timestamp; + } + + @NonNull + public String getDateTime() { + return Utils.datetimeParser.format(new Date(timestamp / 1000L)); + } + + public ProfileModel getProfileModel() { + return profileModel; + } + + public DirectItemLinkModel getLinkModel() { + return linkModel; + } + + public DirectItemMediaModel getMediaModel() { + return mediaModel; + } + + public DirectItemReelShareModel getReelShare() { + return reelShare; + } + + public DirectItemActionLogModel getActionLogModel() { + return actionLogModel; + } + + public DirectItemVoiceMediaModel getVoiceMediaModel() { + return voiceMediaModel; + } + + public DirectItemRavenMediaModel getRavenMediaModel() { + return ravenMediaModel; + } + + public DirectItemAnimatedMediaModel getAnimatedMediaModel() { + return animatedMediaModel; + } + + public DirectItemVideoCallEventModel getVideoCallEventModel() { + return videoCallEventModel; + } + + @Override + public int compareTo(@NonNull final DirectItemModel o) { + return Long.compare(timestamp, o.timestamp); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public final static class DirectItemAnimatedMediaModel implements Serializable { + private final boolean isRandom, isSticker; + private final String id; + private final String gifUrl, webpUrl, mp4Url; + private final int height, width; + + public DirectItemAnimatedMediaModel(final boolean isRandom, final boolean isSticker, final String id, final String gifUrl, + final String webpUrl, final String mp4Url, final int height, final int width) { + this.isRandom = isRandom; + this.isSticker = isSticker; + this.id = id; + this.gifUrl = gifUrl; + this.webpUrl = webpUrl; + this.mp4Url = mp4Url; + this.height = height; + this.width = width; + } + + public boolean isRandom() { + return isRandom; + } + + public boolean isSticker() { + return isSticker; + } + + public String getId() { + return id; + } + + public String getGifUrl() { + return gifUrl; + } + + public String getWebpUrl() { + return webpUrl; + } + + public String getMp4Url() { + return mp4Url; + } + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + } + + public final static class DirectItemVoiceMediaModel implements Serializable { + private final String id, audioUrl; + private final long durationMs; + private final int[] waveformData; + private int progress; + private boolean isPlaying = false; + + public DirectItemVoiceMediaModel(final String id, final String audioUrl, final long durationMs, final int[] waveformData) { + this.id = id; + this.audioUrl = audioUrl; + this.durationMs = durationMs; + this.waveformData = waveformData; + } + + public String getId() { + return id; + } + + public String getAudioUrl() { + return audioUrl; + } + + public long getDurationMs() { + return durationMs; + } + + public int[] getWaveformData() { + return waveformData; + } + + public void setProgress(final int progress) { + this.progress = progress; + } + + public int getProgress() { + return progress; + } + + public boolean isPlaying() { + return isPlaying; + } + + public void setPlaying(final boolean playing) { + isPlaying = playing; + } + } + + public final static class DirectItemLinkModel implements Serializable { + private final String text; + private final String clientContext; + private final String mutationToken; + private final DirectItemLinkContext linkContext; + + public DirectItemLinkModel(final String text, final String clientContext, final String mutationToken, + final DirectItemLinkContext linkContext) { + this.text = text; + this.clientContext = clientContext; + this.mutationToken = mutationToken; + this.linkContext = linkContext; + } + + public String getText() { + return text; + } + + public String getClientContext() { + return clientContext; + } + + public String getMutationToken() { + return mutationToken; + } + + public DirectItemLinkContext getLinkContext() { + return linkContext; + } + } + + public final static class DirectItemLinkContext implements Serializable { + private final String linkUrl; + private final String linkTitle; + private final String linkSummary; + private final String linkImageUrl; + + public DirectItemLinkContext(final String linkUrl, final String linkTitle, final String linkSummary, final String linkImageUrl) { + this.linkUrl = linkUrl; + this.linkTitle = linkTitle; + this.linkSummary = linkSummary; + this.linkImageUrl = linkImageUrl; + } + + public String getLinkUrl() { + return linkUrl; + } + + public String getLinkTitle() { + return linkTitle; + } + + public String getLinkSummary() { + return linkSummary; + } + + public String getLinkImageUrl() { + return linkImageUrl; + } + } + + public final static class DirectItemActionLogModel implements Serializable { + private final String description; + + public DirectItemActionLogModel(final String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + public final static class DirectItemReelShareModel implements Serializable { + private final boolean isReelPersisted; + private final long reelOwnerId; + private final String text; + private final String type; + private final String reelType; + private final String reelName; + private final String reelId; + private final DirectItemMediaModel media; + + public DirectItemReelShareModel(final boolean isReelPersisted, final long reelOwnerId, final String text, final String type, + final String reelType, final String reelName, final String reelId, final DirectItemMediaModel media) { + this.isReelPersisted = isReelPersisted; + this.reelOwnerId = reelOwnerId; + this.text = text; + this.type = type; + this.reelType = reelType; + this.reelName = reelName; + this.reelId = reelId; + this.media = media; + } + + public boolean isReelPersisted() { + return isReelPersisted; + } + + public long getReelOwnerId() { + return reelOwnerId; + } + + public String getText() { + return text; + } + + public String getType() { + return type; + } + + public String getReelType() { + return reelType; + } + + public String getReelName() { + return reelName; + } + + public String getReelId() { + return reelId; + } + + public DirectItemMediaModel getMedia() { + return media; + } + } + + public final static class DirectItemMediaModel implements Serializable { + private final MediaItemType mediaType; + private final long expiringAt, pk; + private final String id, thumbUrl; + private final ProfileModel user; + + public DirectItemMediaModel(final MediaItemType mediaType, final long expiringAt, final long pk, final String id, + final String thumbUrl, final ProfileModel user) { + this.mediaType = mediaType; + this.expiringAt = expiringAt; + this.pk = pk; + this.id = id; + this.thumbUrl = thumbUrl; + this.user = user; + } + + public MediaItemType getMediaType() { + return mediaType; + } + + public long getExpiringAt() { + return expiringAt; + } + + public long getPk() { + return pk; + } + + public String getId() { + return id; + } + + public ProfileModel getUser() { + return user; + } + + public String getThumbUrl() { + return thumbUrl; + } + } + + public final static class DirectItemRavenMediaModel implements Serializable { + private final long expireAtSecs; + private final int playbackDurationSecs; + private final int seenCount; + private final String[] seenUserIds; + private final RavenMediaViewType viewType; + private final DirectItemMediaModel media; + private final RavenExpiringMediaActionSummaryModel expiringMediaActionSummary; + + public DirectItemRavenMediaModel(final long expireAtSecs, final int playbackDurationSecs, final int seenCount, + final String[] seenUserIds, final RavenMediaViewType viewType, final DirectItemMediaModel media, + final RavenExpiringMediaActionSummaryModel expiringMediaActionSummary) { + this.expireAtSecs = expireAtSecs; + this.playbackDurationSecs = playbackDurationSecs; + this.seenCount = seenCount; + this.seenUserIds = seenUserIds; + this.viewType = viewType; + this.media = media; + this.expiringMediaActionSummary = expiringMediaActionSummary; + } + + public long getExpireAtSecs() { + return expireAtSecs; + } + + public int getPlaybackDurationSecs() { + return playbackDurationSecs; + } + + public int getSeenCount() { + return seenCount; + } + + public String[] getSeenUserIds() { + return seenUserIds; + } + + public RavenMediaViewType getViewType() { + return viewType; + } + + public DirectItemMediaModel getMedia() { + return media; + } + + public RavenExpiringMediaActionSummaryModel getExpiringMediaActionSummary() { + return expiringMediaActionSummary; + } + } + + public final static class DirectItemVideoCallEventModel implements Serializable { + private final long videoCallId; + private final boolean hasAudioOnlyCall; + private final String action; + private final String description; + + public DirectItemVideoCallEventModel(final long videoCallId, final boolean hasAudioOnlyCall, final String action, final String description) { + this.videoCallId = videoCallId; + this.hasAudioOnlyCall = hasAudioOnlyCall; + this.action = action; + this.description = description; + } + + public long getVideoCallId() { + return videoCallId; + } + + public boolean isHasAudioOnlyCall() { + return hasAudioOnlyCall; + } + + public String getAction() { + return action; + } + + public String getDescription() { + return description; + } + } + + public final static class RavenExpiringMediaActionSummaryModel implements Serializable { + private final long timestamp; + private final int count; + private final RavenExpiringMediaType type; + + public RavenExpiringMediaActionSummaryModel(final long timestamp, final int count, final RavenExpiringMediaType type) { + this.timestamp = timestamp; + this.count = count; + this.type = type; + } + + public long getTimestamp() { + return timestamp; + } + + public int getCount() { + return count; + } + + public RavenExpiringMediaType getType() { + return type; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/direct_messages/InboxMediaModel.java b/app/src/main/java/awais/instagrabber/models/direct_messages/InboxMediaModel.java new file mode 100755 index 00000000..e4746c41 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/direct_messages/InboxMediaModel.java @@ -0,0 +1,27 @@ +package awais.instagrabber.models.direct_messages; + +import awais.instagrabber.models.enums.MediaItemType; + +public final class InboxMediaModel { + private final MediaItemType mediaType; + private final String mediaId; + private final String displayUrl; + + public InboxMediaModel(final MediaItemType mediaType, final String mediaId, final String displayUrl) { + this.mediaType = mediaType; + this.mediaId = mediaId; + this.displayUrl = displayUrl; + } + + public MediaItemType getMediaType() { + return mediaType; + } + + public String getMediaId() { + return mediaId; + } + + public String getDisplayUrl() { + return displayUrl; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/direct_messages/InboxModel.java b/app/src/main/java/awais/instagrabber/models/direct_messages/InboxModel.java new file mode 100755 index 00000000..48be23f0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/direct_messages/InboxModel.java @@ -0,0 +1,63 @@ +package awais.instagrabber.models.direct_messages; + +public final class InboxModel { + private final boolean hasOlder, hasPendingTopRequests, blendedInboxEnabled; + private final int unseenCount, pendingRequestsCount; + private final long seqId, unseenCountTimestamp; + private final InboxThreadModel[] threads; + private String oldestCursor; + + public InboxModel(final boolean hasOlder, final boolean hasPendingTopRequests, final boolean blendedInboxEnabled, + final int unseenCount, final int pendingRequestsCount, final long seqId, final long unseenCountTimestamp, + final String oldestCursor, final InboxThreadModel[] threads) { + this.hasOlder = hasOlder; + this.hasPendingTopRequests = hasPendingTopRequests; + this.blendedInboxEnabled = blendedInboxEnabled; + this.unseenCount = unseenCount; + this.pendingRequestsCount = pendingRequestsCount; + this.unseenCountTimestamp = unseenCountTimestamp; + this.oldestCursor = oldestCursor; + this.threads = threads; + this.seqId = seqId; + } + + public boolean isHasOlder() { + return hasOlder; + } + + public boolean isHasPendingTopRequests() { + return hasPendingTopRequests; + } + + public boolean isBlendedInboxEnabled() { + return blendedInboxEnabled; + } + + public int getUnseenCount() { + return unseenCount; + } + + public int getPendingRequestsCount() { + return pendingRequestsCount; + } + + public long getUnseenCountTimestamp() { + return unseenCountTimestamp; + } + + public long getSeqId() { + return seqId; + } + + public String getOldestCursor() { + return oldestCursor; + } + + public void setOldestCursor(final String oldestCursor) { + this.oldestCursor = oldestCursor; + } + + public InboxThreadModel[] getThreads() { + return threads; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/direct_messages/InboxThreadModel.java b/app/src/main/java/awais/instagrabber/models/direct_messages/InboxThreadModel.java new file mode 100755 index 00000000..9678023a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/direct_messages/InboxThreadModel.java @@ -0,0 +1,145 @@ +package awais.instagrabber.models.direct_messages; + +import java.io.Serializable; + +import awais.instagrabber.models.ProfileModel; +import awais.instagrabber.models.enums.InboxReadState; + +public final class InboxThreadModel implements Serializable { + private final InboxReadState readState; + private final String threadId, threadV2Id, threadType, threadTitle, newestCursor, oldestCursor, nextCursor, prevCursor; + private final ProfileModel inviter; + private final ProfileModel[] users, leftUsers; + private final DirectItemModel[] items; + private final boolean muted, isPin, isSpam, isGroup, named, pending, archived, canonical, hasOlder, hasNewer; + private final long lastActivityAt; + + public InboxThreadModel(final InboxReadState readState, final String threadId, final String threadV2Id, final String threadType, final String threadTitle, + final String newestCursor, final String oldestCursor, final String nextCursor, final String prevCursor, + final ProfileModel inviter, final ProfileModel[] users, + final ProfileModel[] leftUsers, final DirectItemModel[] items, final boolean muted, + final boolean isPin, final boolean named, final boolean canonical, final boolean pending, + final boolean hasOlder, final boolean hasNewer, final boolean isSpam, final boolean isGroup, + final boolean archived, final long lastActivityAt) { + this.readState = readState; + this.threadId = threadId; + this.threadV2Id = threadV2Id; + this.threadType = threadType; + this.threadTitle = threadTitle; + this.newestCursor = newestCursor; + this.oldestCursor = oldestCursor; + this.nextCursor = nextCursor; + this.prevCursor = prevCursor; + this.inviter = inviter; + this.users = users; + this.leftUsers = leftUsers; + this.items = items; // todo + this.muted = muted; + this.isPin = isPin; + this.named = named; + this.canonical = canonical; + this.pending = pending; + this.hasOlder = hasOlder; + this.hasNewer = hasNewer; + this.isSpam = isSpam; + this.isGroup = isGroup; + this.archived = archived; + this.lastActivityAt = lastActivityAt; + } + + public InboxReadState getReadState() { + return readState; + } + + public String getThreadId() { + return threadId; + } + + public String getThreadV2Id() { + return threadV2Id; + } + + public String getThreadType() { + return threadType; + } + + public String getThreadTitle() { + return threadTitle; + } + + public String getNewestCursor() { + return newestCursor; + } + + public String getOldestCursor() { + return oldestCursor; + } + + public String getNextCursor() { + return nextCursor; + } + + public String getPrevCursor() { + return prevCursor; + } + + public ProfileModel getInviter() { + return inviter; + } + + public ProfileModel[] getUsers() { + return users; + } + + public ProfileModel[] getLeftUsers() { + return leftUsers; + } + + public DirectItemModel[] getItems() { + return items; + } + + public boolean isMuted() { + return muted; + } + + public boolean isPin() { + return isPin; + } + + public boolean isNamed() { + return named; + } + + public boolean isPending() { + return pending; + } + + public boolean isArchived() { + return archived; + } + + public boolean isCanonical() { + return canonical; + } + + public boolean isHasOlder() { + return hasOlder; + } + + public boolean isHasNewer() { + return hasNewer; + } + + public boolean isSpam() { + return isSpam; + } + + public boolean isGroup() { + return isGroup; + } + + public long getLastActivityAt() { + return lastActivityAt; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/DirectItemType.java b/app/src/main/java/awais/instagrabber/models/enums/DirectItemType.java new file mode 100755 index 00000000..ee34f7f2 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/DirectItemType.java @@ -0,0 +1,19 @@ +package awais.instagrabber.models.enums; + +import java.io.Serializable; + +public enum DirectItemType implements Serializable { + TEXT, + LIKE, + LINK, + MEDIA, + RAVEN_MEDIA, + PROFILE, + VIDEO_CALL_EVENT, + ANIMATED_MEDIA, + VOICE_MEDIA, + MEDIA_SHARE, + REEL_SHARE, + ACTION_LOG, + PLACEHOLDER, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/DownloadMethod.java b/app/src/main/java/awais/instagrabber/models/enums/DownloadMethod.java new file mode 100755 index 00000000..9c3c9595 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/DownloadMethod.java @@ -0,0 +1,9 @@ +package awais.instagrabber.models.enums; + +public enum DownloadMethod { + DOWNLOAD_MAIN, + DOWNLOAD_DISCOVER, + DOWNLOAD_FEED, + DOWNLOAD_POST_VIEWER, + DOWNLOAD_DIRECT; +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/InboxReadState.java b/app/src/main/java/awais/instagrabber/models/enums/InboxReadState.java new file mode 100755 index 00000000..0924b435 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/InboxReadState.java @@ -0,0 +1,8 @@ +package awais.instagrabber.models.enums; + +import java.io.Serializable; + +public enum InboxReadState implements Serializable { + STATE_READ, + STATE_UNREAD, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/IntentModelType.java b/app/src/main/java/awais/instagrabber/models/enums/IntentModelType.java new file mode 100755 index 00000000..5d17b167 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/IntentModelType.java @@ -0,0 +1,8 @@ +package awais.instagrabber.models.enums; + +public enum IntentModelType { + UNKNOWN, + USERNAME, + POST, + HASHTAG, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/ItemGetType.java b/app/src/main/java/awais/instagrabber/models/enums/ItemGetType.java new file mode 100755 index 00000000..b6e7055b --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/ItemGetType.java @@ -0,0 +1,9 @@ +package awais.instagrabber.models.enums; + +import java.io.Serializable; + +public enum ItemGetType implements Serializable { + MAIN_ITEMS, + DISCOVER_ITEMS, + FEED_ITEMS, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java b/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java new file mode 100755 index 00000000..97433550 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java @@ -0,0 +1,10 @@ +package awais.instagrabber.models.enums; + +import java.io.Serializable; + +public enum MediaItemType implements Serializable { + MEDIA_TYPE_IMAGE, + MEDIA_TYPE_VIDEO, + MEDIA_TYPE_SLIDER, + MEDIA_TYPE_VOICE, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/ProfilePictureFetchMode.java b/app/src/main/java/awais/instagrabber/models/enums/ProfilePictureFetchMode.java new file mode 100755 index 00000000..d60f590e --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/ProfilePictureFetchMode.java @@ -0,0 +1,7 @@ +package awais.instagrabber.models.enums; + +public enum ProfilePictureFetchMode { + INSTADP, + INSTA_STALKER, + INSTAFULLSIZE, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/RavenExpiringMediaType.java b/app/src/main/java/awais/instagrabber/models/enums/RavenExpiringMediaType.java new file mode 100755 index 00000000..889a5ccf --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/RavenExpiringMediaType.java @@ -0,0 +1,15 @@ +package awais.instagrabber.models.enums; + +// thanks to http://github.com/warifp/InstagramAutoPostImageUrl/blob/master/vendor/mgp25/instagram-php/src/Response/Model/ActionBadge.php +public enum RavenExpiringMediaType { + RAVEN_DELIVERED, + RAVEN_SENT, + RAVEN_OPENED, + RAVEN_SCREENSHOT, + RAVEN_REPLAYED, + RAVEN_CANNOT_DELIVER, + RAVEN_SENDING, + RAVEN_BLOCKED, + RAVEN_UNKNOWN, + RAVEN_SUGGESTED, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/RavenMediaViewType.java b/app/src/main/java/awais/instagrabber/models/enums/RavenMediaViewType.java new file mode 100755 index 00000000..4e45649c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/RavenMediaViewType.java @@ -0,0 +1,7 @@ +package awais.instagrabber.models.enums; + +public enum RavenMediaViewType { + PERMANENT, + REPLAYABLE, + ONCE, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/SuggestionType.java b/app/src/main/java/awais/instagrabber/models/enums/SuggestionType.java new file mode 100755 index 00000000..917e1e60 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/SuggestionType.java @@ -0,0 +1,6 @@ +package awais.instagrabber.models.enums; + +public enum SuggestionType { + TYPE_USER, + TYPE_HASHTAG, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/UserInboxDirection.java b/app/src/main/java/awais/instagrabber/models/enums/UserInboxDirection.java new file mode 100755 index 00000000..620c50f0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/UserInboxDirection.java @@ -0,0 +1,6 @@ +package awais.instagrabber.models.enums; + +public enum UserInboxDirection { + OLDER, + NEWER, +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/ChangelogFetcher.java b/app/src/main/java/awais/instagrabber/utils/ChangelogFetcher.java new file mode 100755 index 00000000..b1e0740f --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/ChangelogFetcher.java @@ -0,0 +1,50 @@ +package awais.instagrabber.utils; + +import android.os.AsyncTask; +import android.util.Log; + +import java.net.HttpURLConnection; +import java.net.URL; + +import awais.instagrabber.BuildConfig; +import awais.instagrabber.interfaces.FetchListener; + +public final class ChangelogFetcher extends AsyncTask { + private final FetchListener fetchListener; + + public ChangelogFetcher(final FetchListener fetchListener) { + this.fetchListener = fetchListener; + } + + @Override + protected String doInBackground(final Void... voids) { + String result = null; + final String changelogUrl = "https://gitlab.com/AwaisKing/instagrabber/-/raw/master/CHANGELOG"; + + try { + final HttpURLConnection conn = (HttpURLConnection) new URL(changelogUrl).openConnection(); + conn.setUseCaches(false); + conn.connect(); + + if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { + result = Utils.readFromConnection(conn); + } + + conn.disconnect(); + } catch (final Exception e) { + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + + return result; + } + + @Override + protected void onPreExecute() { + if (fetchListener != null) fetchListener.doBefore(); + } + + @Override + protected void onPostExecute(final String result) { + if (fetchListener != null) fetchListener.onResult(result); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java new file mode 100755 index 00000000..3286aed2 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -0,0 +1,45 @@ +package awais.instagrabber.utils; + +public final class Constants { + // string prefs + public static final String FOLDER_PATH = "custom_path"; + public static final String DATE_TIME_FORMAT = "date_time_format"; + public static final String DATE_TIME_SELECTION = "date_time_selection"; + public static final String CUSTOM_DATE_TIME_FORMAT = "date_time_custom_format"; + // int prefs + public static final String APP_THEME = "app_theme"; + public static final String APP_LANGUAGE = "app_language"; + public static final String PREV_INSTALL_VERSION = "prevVersion"; + public static final String PROFILE_FETCH_MODE = "profile_fetch_mode"; + // boolean prefs + public static final String DOWNLOAD_USER_FOLDER = "download_user_folder"; + public static final String BOTTOM_TOOLBAR = "bottom_toolbar"; + public static final String FOLDER_SAVE_TO = "saved_to"; + public static final String AUTOPLAY_VIDEOS = "autoplay_videos"; + public static final String MUTED_VIDEOS = "muted_videos"; + public static final String AUTOLOAD_POSTS = "autoload_posts"; + public static final String SHOW_FEED = "show_feed"; + public static final String CUSTOM_DATE_TIME_FORMAT_ENABLED = "data_time_custom_enabled"; + // never Export + public static final String COOKIE = "cookie"; + public static final String SHOW_QUICK_ACCESS_DIALOG = "show_quick_dlg"; + //////////////////////// EXTRAS //////////////////////// + public static final String EXTRAS_USER = "user"; + public static final String EXTRAS_USERNAME = "username"; + public static final String EXTRAS_ID = "id"; + public static final String EXTRAS_POST = "post"; + public static final String EXTRAS_PROFILE = "profile"; + public static final String EXTRAS_TYPE = "type"; + public static final String EXTRAS_NAME = "name"; + public static final String EXTRAS_STORIES = "stories"; + public static final String EXTRAS_HIGHLIGHT = "highlight"; + public static final String EXTRAS_INDEX = "index"; + public static final String EXTRAS_THREAD_MODEL = "threadModel"; + public static final String EXTRAS_FOLLOWERS = "followers"; + public static final String EXTRAS_SHORTCODE = "shortcode"; + public static final String EXTRAS_END_CURSOR = "endCursor"; + //////////////////////// EXTRAS //////////////////////// + public static final String 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)"; +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/DataBox.java b/app/src/main/java/awais/instagrabber/utils/DataBox.java new file mode 100755 index 00000000..1189bdd0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/DataBox.java @@ -0,0 +1,271 @@ +package awais.instagrabber.utils; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; + +import awais.instagrabber.BuildConfig; +import awaisomereport.LogCollector; + +import static awais.instagrabber.utils.Utils.logCollector; + +public final class DataBox extends SQLiteOpenHelper { + private static DataBox sInstance; + private final static int VERSION = 1; + private final static String TABLE_COOKIES = "cookies"; + private final static String TABLE_FAVORITES = "favorites"; + private final static String KEY_DATE_ADDED = "date_added"; + private final static String KEY_QUERY_TEXT = "query_text"; + private final static String KEY_USERNAME = Constants.EXTRAS_USERNAME; + private final static String KEY_COOKIE = "cookie"; + private final static String KEY_UID = "uid"; + + public static synchronized DataBox getInstance(final Context context) { + if (sInstance == null) sInstance = new DataBox(context.getApplicationContext()); + return sInstance; + } + + public DataBox(@Nullable final Context context) { + super(context, "cookiebox.db", null, VERSION); + } + + @Override + public void onCreate(@NonNull final SQLiteDatabase db) { + db.execSQL("CREATE TABLE cookies (id INTEGER PRIMARY KEY, uid TEXT, username TEXT, cookie TEXT)"); + db.execSQL("CREATE TABLE favorites (id INTEGER PRIMARY KEY, query_text TEXT, date_added INTEGER)"); + } + + @Override + public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { } + + ///////////////////////////////////////// YOUR WEIRD FETIS-FAVORITES! HERE ///////////////////////////////////////// + public final void addFavorite(@NonNull final FavoriteModel favoriteModel) { + final String query = favoriteModel.getQuery(); + if (!Utils.isEmpty(query)) { + try (final SQLiteDatabase db = getWritableDatabase()) { + db.beginTransaction(); + try { + final ContentValues values = new ContentValues(); + values.put(KEY_DATE_ADDED, favoriteModel.getDate()); + values.put(KEY_QUERY_TEXT, query); + + final int rows = db.update(TABLE_FAVORITES, values, KEY_QUERY_TEXT + "=?", new String[]{query}); + + if (rows != 1) + db.insertOrThrow(TABLE_FAVORITES, null, values); + + db.setTransactionSuccessful(); + } catch (final Exception e) { + if (logCollector != null) + logCollector.appendException(e, LogCollector.LogFile.DATA_BOX_FAVORITES, "addFavorite"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } finally { + db.endTransaction(); + } + } + } + } + + public final synchronized void delFavorite(@NonNull final FavoriteModel favoriteModel) { + final String query = favoriteModel.getQuery(); + if (!Utils.isEmpty(query)) { + try (final SQLiteDatabase db = getWritableDatabase()) { + db.beginTransaction(); + try { + final int rowsDeleted = db.delete(TABLE_FAVORITES, KEY_QUERY_TEXT + "=? AND " + KEY_DATE_ADDED + "=?", + new String[]{query, Long.toString(favoriteModel.getDate())}); + + if (rowsDeleted > 0) db.setTransactionSuccessful(); + } catch (final Exception e) { + if (logCollector != null) + logCollector.appendException(e, LogCollector.LogFile.DATA_BOX_FAVORITES, "delFavorite"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } finally { + db.endTransaction(); + } + } + } + } + + @Nullable + public final ArrayList getAllFavorites() { + ArrayList favorites = null; + + try (final SQLiteDatabase db = getReadableDatabase(); + final Cursor cursor = db.rawQuery("SELECT query_text, date_added FROM favorites ORDER BY date_added DESC", null)) { + if (cursor != null && cursor.moveToFirst()) { + favorites = new ArrayList<>(); + do { + favorites.add(new FavoriteModel( + cursor.getString(0), // query text + cursor.getLong(1) // date added + )); + } while (cursor.moveToNext()); + } + } + + return favorites; + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + ///////////////////////////////////// YOUR COOKIES FOR COOKIE MONSTER ARE HERE ///////////////////////////////////// + public final void addUserCookie(@NonNull final CookieModel cookieModel) { + final String cookieModelUid = cookieModel.getUid(); + if (!Utils.isEmpty(cookieModelUid)) { + try (final SQLiteDatabase db = getWritableDatabase()) { + db.beginTransaction(); + try { + final ContentValues values = new ContentValues(); + values.put(KEY_USERNAME, cookieModel.getUsername()); + values.put(KEY_COOKIE, cookieModel.getCookie()); + values.put(KEY_UID, cookieModelUid); + + final int rows = db.update(TABLE_COOKIES, values, KEY_UID + "=?", new String[]{cookieModelUid}); + + if (rows != 1) + db.insertOrThrow(TABLE_COOKIES, null, values); + + db.setTransactionSuccessful(); + } catch (final Exception e) { + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } finally { + db.endTransaction(); + } + } + } + } + + public final synchronized void delUserCookie(@NonNull final CookieModel cookieModel) { + final String cookieModelUid = cookieModel.getUid(); + if (!Utils.isEmpty(cookieModelUid)) { + try (final SQLiteDatabase db = getWritableDatabase()) { + db.beginTransaction(); + try { + final int rowsDeleted = db.delete(TABLE_COOKIES, KEY_UID + "=? AND " + KEY_USERNAME + "=? AND " + KEY_COOKIE + "=?", + new String[]{cookieModelUid, cookieModel.getUsername(), cookieModel.getCookie()}); + + if (rowsDeleted > 0) db.setTransactionSuccessful(); + } catch (final Exception e) { + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } finally { + db.endTransaction(); + } + } + } + } + + public final int getCookieCount() { + int cookieCount = 0; + try (final SQLiteDatabase db = getReadableDatabase(); + final Cursor cursor = db.rawQuery("SELECT * FROM cookies", null)) { + if (cursor != null) cookieCount = cursor.getCount(); + } + return cookieCount; + } + + @Nullable + public final CookieModel getCookie(final String uid) { + CookieModel cookie = null; + try (final SQLiteDatabase db = getReadableDatabase(); + final Cursor cursor = db.rawQuery("SELECT uid, username, cookie FROM cookies WHERE uid = ?", new String[]{uid})) { + if (cursor != null && cursor.moveToFirst()) + cookie = new CookieModel( + cursor.getString(0), // uid + cursor.getString(1), // username + cursor.getString(2) // cookie + ); + } + return cookie; + } + + @Nullable + public final ArrayList getAllCookies() { + ArrayList cookies = null; + + try (final SQLiteDatabase db = getReadableDatabase(); + final Cursor cursor = db.rawQuery("SELECT uid, username, cookie FROM cookies", null)) { + if (cursor != null && cursor.moveToFirst()) { + cookies = new ArrayList<>(); + do { + cookies.add(new CookieModel( + cursor.getString(0), // uid + cursor.getString(1), // username + cursor.getString(2) // cookie + )); + } while (cursor.moveToNext()); + } + } + + return cookies; + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public static class CookieModel { + private final String uid, username, cookie; + private boolean selected; + + public CookieModel(final String uid, final String username, final String cookie) { + this.uid = uid; + this.username = username; + this.cookie = cookie; + } + + public String getUid() { + return uid; + } + + public String getUsername() { + return username; + } + + public String getCookie() { + return cookie; + } + + public boolean isSelected() { + return selected; + } + + public void setSelected(final boolean selected) { + this.selected = selected; + } + + @NonNull + @Override + public String toString() { + return username; + } + } + + public static class FavoriteModel { + private final String query; + private final long date; + + public FavoriteModel(final String query, final long date) { + this.query = query; + this.date = date; + } + + public String getQuery() { + return query; + } + + public long getDate() { + return date; + } + + @NonNull + @Override + public String toString() { + return query; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java b/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java new file mode 100755 index 00000000..9c912272 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java @@ -0,0 +1,248 @@ +package awais.instagrabber.utils; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.os.Environment; +import android.os.FileObserver; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.SimpleAdapter; + +public final class DirectoryChooser extends DialogFragment { + public static final String KEY_CURRENT_DIRECTORY = "CURRENT_DIRECTORY"; + private static final File sdcardPathFile = Environment.getExternalStorageDirectory(); + private static final String sdcardPath = sdcardPathFile.getPath(); + private final List fileNames = new ArrayList<>(); + private Context context; + private View btnConfirm, btnNavUp, btnCancel; + private File selectedDir; + private String initialDirectory; + private TextView tvSelectedFolder; + private FileObserver fileObserver; + private SimpleAdapter listDirectoriesAdapter; + private OnFragmentInteractionListener interactionListener; + private boolean showZaAiConfigFiles = false; + + public DirectoryChooser() { + super(); + } + + public DirectoryChooser setInitialDirectory(final String initialDirectory) { + if (!Utils.isEmpty(initialDirectory)) + this.initialDirectory = initialDirectory; + return this; + } + + public DirectoryChooser setShowZaAiConfigFiles(final boolean showZaAiConfigFiles) { + this.showZaAiConfigFiles = showZaAiConfigFiles; + return this; + } + + @Override + public void onAttach(@NonNull final Context context) { + super.onAttach(context); + + this.context = context; + + if (this.context instanceof OnFragmentInteractionListener) + interactionListener = (OnFragmentInteractionListener) this.context; + else { + final Fragment owner = getTargetFragment(); + if (owner instanceof OnFragmentInteractionListener) + interactionListener = (OnFragmentInteractionListener) owner; + } + } + + @NonNull + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + Context context = this.context; + if (context == null) context = getContext(); + if (context == null) context = getActivity(); + if (context == null) context = inflater.getContext(); + + final View view = inflater.inflate(R.layout.layout_directory_chooser, container, false); + + btnNavUp = view.findViewById(R.id.btnNavUp); + btnCancel = view.findViewById(R.id.btnCancel); + btnConfirm = view.findViewById(R.id.btnConfirm); + tvSelectedFolder = view.findViewById(R.id.txtvSelectedFolder); + + final View.OnClickListener clickListener = v -> { + final Object tag; + if (v instanceof TextView && (tag = v.getTag()) instanceof CharSequence) { + final File file = new File(selectedDir, tag.toString()); + if (file.isDirectory()) + changeDirectory(file); + else if (showZaAiConfigFiles && file.isFile()) { + if (interactionListener != null && file.canRead()) + interactionListener.onSelectDirectory(file.getAbsolutePath()); + dismiss(); + } + + } else if (v == btnNavUp) { + final File parent; + if (selectedDir != null && (parent = selectedDir.getParentFile()) != null) + changeDirectory(parent); + + } else if (v == btnConfirm) { + if (interactionListener != null && isValidFile(selectedDir)) + interactionListener.onSelectDirectory(selectedDir.getAbsolutePath()); + dismiss(); + } else if (v == btnCancel) { + dismiss(); + } + }; + + btnNavUp.setOnClickListener(clickListener); + btnCancel.setOnClickListener(clickListener); + btnConfirm.setOnClickListener(clickListener); + + listDirectoriesAdapter = new SimpleAdapter<>(context, fileNames, clickListener); + + final RecyclerView directoriesList = view.findViewById(R.id.directoryList); + directoriesList.setLayoutManager(new LinearLayoutManager(context)); + directoriesList.setAdapter(listDirectoriesAdapter); + + final File initDir = new File(initialDirectory); + final File initialDir = !Utils.isEmpty(initialDirectory) && isValidFile(initDir) ? initDir : Environment.getExternalStorageDirectory(); + + changeDirectory(initialDir); + + return view; + } + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (Utils.isEmpty(initialDirectory)) { + initialDirectory = new File(sdcardPath, "Download").getAbsolutePath(); + if (savedInstanceState != null) { + final String savedDir = savedInstanceState.getString(KEY_CURRENT_DIRECTORY); + if (!Utils.isEmpty(savedDir)) initialDirectory = savedDir; + } + } + + setStyle(DialogFragment.STYLE_NO_TITLE, 0); + } + + @NonNull + @Override + public Dialog onCreateDialog(final Bundle savedInstanceState) { + return new Dialog(context, R.attr.alertDialogTheme) { + @Override + public void onBackPressed() { + if (selectedDir != null) { + final String absolutePath = selectedDir.getAbsolutePath(); + if (absolutePath.equals(sdcardPath) || absolutePath.equals(sdcardPathFile.getAbsolutePath())) + dismiss(); + else + changeDirectory(selectedDir.getParentFile()); + } + } + }; + } + + @Override + public void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); + if (selectedDir != null) outState.putString(KEY_CURRENT_DIRECTORY, selectedDir.getAbsolutePath()); + } + + @Override + public void onResume() { + super.onResume(); + if (fileObserver != null) fileObserver.startWatching(); + } + + @Override + public void onPause() { + super.onPause(); + if (fileObserver != null) fileObserver.stopWatching(); + } + + @Override + public void onDetach() { + super.onDetach(); + interactionListener = null; + } + + private void changeDirectory(final File dir) { + if (dir != null && dir.isDirectory()) { + final String path = dir.getAbsolutePath(); + + final File[] contents = dir.listFiles(); + if (contents != null) { + fileNames.clear(); + + for (final File f : contents) { + final String name = f.getName(); + if (f.isDirectory() || showZaAiConfigFiles && f.isFile() && name.toLowerCase().endsWith(".zaai")) + fileNames.add(name); + } + + Collections.sort(fileNames); + selectedDir = dir; + tvSelectedFolder.setText(path); + listDirectoriesAdapter.notifyDataSetChanged(); + fileObserver = new FileObserver(path, FileObserver.CREATE | FileObserver.DELETE | FileObserver.MOVED_FROM | FileObserver.MOVED_TO) { + private final Runnable currentDirRefresher = () -> changeDirectory(selectedDir); + + @Override + public void onEvent(final int event, final String path) { + if (context instanceof Activity) ((Activity) context).runOnUiThread(currentDirRefresher); + } + }; + fileObserver.startWatching(); + } + } + refreshButtonState(); + } + + private void refreshButtonState() { + if (selectedDir != null) { + final String path = selectedDir.getAbsolutePath(); + toggleUpButton(!path.equals(sdcardPathFile.getAbsolutePath()) && selectedDir != sdcardPathFile); + btnConfirm.setEnabled(isValidFile(selectedDir)); + } + } + + private void toggleUpButton(final boolean enable) { + if (btnNavUp != null) { + btnNavUp.setEnabled(enable); + btnNavUp.setAlpha(enable ? 1f : 0.617f); + } + } + + private boolean isValidFile(final File file) { + return file != null && file.isDirectory() && file.canRead(); + } + + public DirectoryChooser setInteractionListener(final OnFragmentInteractionListener interactionListener) { + this.interactionListener = interactionListener; + return this; + } + + public interface OnFragmentInteractionListener { + void onSelectDirectory(final String path); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java new file mode 100755 index 00000000..25f0b6a3 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java @@ -0,0 +1,328 @@ +package awais.instagrabber.utils; + +import android.content.Context; +import android.text.InputFilter; +import android.text.InputType; +import android.util.Base64; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatEditText; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.Iterator; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import awais.instagrabber.BuildConfig; +import awais.instagrabber.R; +import awais.instagrabber.interfaces.FetchListener; +import awaisomereport.LogCollector.LogFile; + +import static awais.instagrabber.utils.Utils.logCollector; +import static awais.instagrabber.utils.Utils.settingsHelper; + +public final class ExportImportUtils { + public static final int FLAG_COOKIES = 1; + public static final int FLAG_FAVORITES = 1 << 1; + public static final int FLAG_SETTINGS = 1 << 2; + + @IntDef(value = {FLAG_COOKIES, FLAG_FAVORITES, FLAG_SETTINGS}, flag = true) + @interface ExportImportFlags {} + + public static void Export(@Nullable final String password, @ExportImportFlags final int flags, @NonNull final File filePath, + final FetchListener fetchListener) { + final String exportString = ExportImportUtils.getExportString(flags); + if (!Utils.isEmpty(exportString)) { + final boolean isPass = !Utils.isEmpty(password); + byte[] exportBytes = null; + + if (isPass) { + final byte[] passwordBytes = password.getBytes(); + final byte[] bytes = new byte[32]; + System.arraycopy(passwordBytes, 0, bytes, 0, Math.min(passwordBytes.length, 32)); + + try { + exportBytes = PasswordUtils.enc(exportString, bytes); + } catch (final Exception e) { + if (fetchListener != null) fetchListener.onResult(false); + if (logCollector != null) + logCollector.appendException(e, LogFile.UTILS_EXPORT, "Export::isPass"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } else { + exportBytes = Base64.encode(exportString.getBytes(), Base64.DEFAULT | Base64.NO_WRAP | Base64.NO_PADDING); + } + + if (exportBytes != null && exportBytes.length > 1) { + try (final FileOutputStream fos = new FileOutputStream(filePath)) { + fos.write(isPass ? 'A' : 'Z'); + fos.write(exportBytes); + if (fetchListener != null) fetchListener.onResult(true); + } catch (final Exception e) { + if (fetchListener != null) fetchListener.onResult(false); + if (logCollector != null) + logCollector.appendException(e, LogFile.UTILS_EXPORT, "Export::notPass"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } else if (fetchListener != null) fetchListener.onResult(false); + } + } + + public static void Import(@NonNull final Context context, @ExportImportFlags final int flags, @NonNull final File filePath, + final FetchListener fetchListener) { + try (final FileInputStream fis = new FileInputStream(filePath)) { + final int configType = fis.read(); + + final StringBuilder builder = new StringBuilder(); + int c; + while ((c = fis.read()) != -1) { + builder.append((char) c); + } + + if (configType == 'A') { + // password + final AppCompatEditText editText = new AppCompatEditText(context); + editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(32)}); + editText.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); + new AlertDialog.Builder(context).setView(editText).setTitle(R.string.password).setPositiveButton(R.string.confirm, (dialog, which) -> { + final CharSequence text = editText.getText(); + if (!Utils.isEmpty(text)) { + try { + final byte[] passwordBytes = text.toString().getBytes(); + final byte[] bytes = new byte[32]; + System.arraycopy(passwordBytes, 0, bytes, 0, Math.min(passwordBytes.length, 32)); + saveToSettings(new String(PasswordUtils.dec(builder.toString(), bytes)), flags, fetchListener); + } catch (final Exception e) { + if (fetchListener != null) fetchListener.onResult(false); + if (logCollector != null) + logCollector.appendException(e, LogFile.UTILS_IMPORT, "Import::pass"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + + } else + Toast.makeText(context, R.string.dialog_export_err_password_empty, Toast.LENGTH_SHORT).show(); + }).show(); + + } else if (configType == 'Z') { + saveToSettings(new String(Base64.decode(builder.toString(), Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP)), + flags, fetchListener); + + } else { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + if (fetchListener != null) fetchListener.onResult(false); + } + } catch (final Exception e) { + if (fetchListener != null) fetchListener.onResult(false); + if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_IMPORT, "Import"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } + + private static void saveToSettings(final String json, @ExportImportFlags final int flags, final FetchListener fetchListener) { + try { + final JSONObject jsonObject = new JSONObject(json); + + if ((flags & FLAG_SETTINGS) == FLAG_SETTINGS && jsonObject.has("settings")) { + final JSONObject objSettings = jsonObject.getJSONObject("settings"); + final Iterator keys = objSettings.keys(); + while (keys.hasNext()) { + final String key = keys.next(); + final Object val = objSettings.opt(key); + if (val instanceof String) { + settingsHelper.putString(key, (String) val); + } else if (val instanceof Integer) { + settingsHelper.putInteger(key, (int) val); + } else if (val instanceof Boolean) { + settingsHelper.putBoolean(key, (boolean) val); + } + } + } + + if ((flags & FLAG_COOKIES) == FLAG_COOKIES && jsonObject.has("cookies")) { + final JSONArray cookies = jsonObject.getJSONArray("cookies"); + final int cookiesLen = cookies.length(); + for (int i = 0; i < cookiesLen; ++i) { + final JSONObject cookieObject = cookies.getJSONObject(i); + Utils.dataBox.addUserCookie(new DataBox.CookieModel(cookieObject.getString("i"), + cookieObject.getString("u"), cookieObject.getString("c"))); + } + } + + if ((flags & FLAG_FAVORITES) == FLAG_FAVORITES && jsonObject.has("favs")) { + final JSONArray favs = jsonObject.getJSONArray("favs"); + final int favsLen = favs.length(); + for (int i = 0; i < favsLen; ++i) { + final JSONObject favsObject = favs.getJSONObject(i); + Utils.dataBox.addFavorite(new DataBox.FavoriteModel(favsObject.getString("q"), + favsObject.getLong("d"))); + } + } + + if (fetchListener != null) fetchListener.onResult(true); + + } catch (final Exception e) { + if (fetchListener != null) fetchListener.onResult(false); + if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_IMPORT, "saveToSettings"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } + + @Nullable + private static String getExportString(@ExportImportFlags final int flags) { + String result = null; + try { + final JSONObject jsonObject = new JSONObject(); + + String str; + if ((flags & FLAG_SETTINGS) == FLAG_SETTINGS) { + str = getSettings(); + if (str != null) jsonObject.put("settings", new JSONObject(str)); + } + + if ((flags & FLAG_COOKIES) == FLAG_COOKIES) { + str = getCookies(); + if (str != null) jsonObject.put("cookies", new JSONArray(str)); + } + + if ((flags & FLAG_FAVORITES) == FLAG_FAVORITES) { + str = getFavorites(); + if (str != null) jsonObject.put("favs", new JSONArray(str)); + } + + result = jsonObject.toString(); + } catch (final Exception e) { + if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_EXPORT, "getExportString"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + return result; + } + + @Nullable + private static String getSettings() { + String result = null; + + if (settingsHelper != null) { + try { + final JSONObject json = new JSONObject(); + json.put(Constants.APP_THEME, settingsHelper.getInteger(Constants.APP_THEME)); + json.put(Constants.APP_LANGUAGE, settingsHelper.getInteger(Constants.APP_LANGUAGE)); + json.put(Constants.PROFILE_FETCH_MODE, settingsHelper.getInteger(Constants.PROFILE_FETCH_MODE)); + + String str = settingsHelper.getString(Constants.FOLDER_PATH); + if (!Utils.isEmpty(str)) json.put(Constants.FOLDER_PATH, str); + + str = settingsHelper.getString(Constants.DATE_TIME_FORMAT); + if (!Utils.isEmpty(str)) json.put(Constants.DATE_TIME_FORMAT, str); + + str = settingsHelper.getString(Constants.DATE_TIME_SELECTION); + if (!Utils.isEmpty(str)) json.put(Constants.DATE_TIME_SELECTION, str); + + str = settingsHelper.getString(Constants.CUSTOM_DATE_TIME_FORMAT); + if (!Utils.isEmpty(str)) json.put(Constants.CUSTOM_DATE_TIME_FORMAT, str); + + json.put(Constants.DOWNLOAD_USER_FOLDER, settingsHelper.getBoolean(Constants.DOWNLOAD_USER_FOLDER)); + json.put(Constants.MUTED_VIDEOS, settingsHelper.getBoolean(Constants.MUTED_VIDEOS)); + json.put(Constants.BOTTOM_TOOLBAR, settingsHelper.getBoolean(Constants.BOTTOM_TOOLBAR)); + json.put(Constants.AUTOPLAY_VIDEOS, settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS)); + json.put(Constants.SHOW_FEED, settingsHelper.getBoolean(Constants.SHOW_FEED)); + json.put(Constants.AUTOLOAD_POSTS, settingsHelper.getBoolean(Constants.AUTOLOAD_POSTS)); + json.put(Constants.FOLDER_SAVE_TO, settingsHelper.getBoolean(Constants.FOLDER_SAVE_TO)); + + result = json.toString(); + } catch (final Exception e) { + result = null; + if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_EXPORT, "getSettings"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } + + return result; + } + + @Nullable + private static String getFavorites() { + String result = null; + if (Utils.dataBox != null) { + try { + final ArrayList allFavorites = Utils.dataBox.getAllFavorites(); + final int allFavoritesSize; + if (allFavorites != null && (allFavoritesSize = allFavorites.size()) > 0) { + final JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < allFavoritesSize; i++) { + final DataBox.FavoriteModel favorite = allFavorites.get(i); + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("q", favorite.getQuery()); + jsonObject.put("d", favorite.getDate()); + jsonArray.put(jsonObject); + } + result = jsonArray.toString(); + } + } catch (final Exception e) { + result = null; + if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_EXPORT, "getFavorites"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } + return result; + } + + @Nullable + private static String getCookies() { + String result = null; + if (Utils.dataBox != null) { + try { + final ArrayList allCookies = Utils.dataBox.getAllCookies(); + final int allCookiesSize; + if (allCookies != null && (allCookiesSize = allCookies.size()) > 0) { + final JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < allCookiesSize; i++) { + final DataBox.CookieModel cookieModel = allCookies.get(i); + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("i", cookieModel.getUid()); + jsonObject.put("u", cookieModel.getUsername()); + jsonObject.put("c", cookieModel.getCookie()); + jsonArray.put(jsonObject); + } + result = jsonArray.toString(); + } + } catch (final Exception e) { + result = null; + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } + return result; + } + + private final static class PasswordUtils { + private static final String cipherAlgo = "AES"; + private static final String cipherTran = "AES/CBC/PKCS5Padding"; + + private static byte[] dec(final String encrypted, final byte[] keyValue) throws Exception { + final Cipher cipher = Cipher.getInstance(cipherTran); + final SecretKeySpec secretKey = new SecretKeySpec(keyValue, cipherAlgo); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(new byte[16])); + return cipher.doFinal(Base64.decode(encrypted, Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP)); + } + + private static byte[] enc(@NonNull final String str, final byte[] keyValue) throws Exception { + final Cipher cipher = Cipher.getInstance(cipherTran); + final SecretKeySpec secretKey = new SecretKeySpec(keyValue, cipherAlgo); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(new byte[16])); + final byte[] bytes = cipher.doFinal(str.getBytes()); + return Base64.encode(bytes, Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/FlavorTown.java b/app/src/main/java/awais/instagrabber/utils/FlavorTown.java new file mode 100755 index 00000000..8668cf9c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/FlavorTown.java @@ -0,0 +1,98 @@ +package awais.instagrabber.utils; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncTask; +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.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; + +import awais.instagrabber.BuildConfig; +import awais.instagrabber.R; +import awais.instagrabber.interfaces.FetchListener; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public final class FlavorTown { + public static void updateCheck(@NonNull final Context context) { + new UpdateChecker(versionUrl -> { + new AlertDialog.Builder(context).setTitle(R.string.update_available).setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.action_download, (dialog, which) -> { + try { + context.startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(versionUrl))); + } catch (final ActivityNotFoundException e) { + // do nothing + } + }).show(); + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + public static void changelogCheck(@NonNull final Context context) { + if (settingsHelper.getInteger(Constants.PREV_INSTALL_VERSION) < BuildConfig.VERSION_CODE) { + new ChangelogFetcher(new FetchListener() { + private AlertDialog alertDialog; + private TextView textView; + + @Override + public void doBefore() { + final ViewGroup rootView = (ViewGroup) View.inflate(context, R.layout.layout_changelog_textview, null); + textView = (TextView) rootView.getChildAt(0); + textView.setMovementMethod(new LinkMovementMethod()); + alertDialog = new AlertDialog.Builder(context).setTitle(R.string.title_changelog).setView(rootView).create(); + } + + @Override + public void onResult(final CharSequence result) { + if (alertDialog != null && textView != null && !Utils.isEmpty(result)) { + final Resources resources = context.getResources(); + + final SpannableStringBuilder stringBuilder = new SpannableStringBuilder( + resources.getString(R.string.curr_version, BuildConfig.VERSION_NAME)) + .append('\n'); + + stringBuilder.setSpan(new RelativeSizeSpan(1.3f), 0, stringBuilder.length() - 1, 0); + + final int resLen = result.length(); + int versionTimes = 0; + + for (int i = 0; i < resLen; ++i) { + final char c = result.charAt(i); + + if (c == 'v' && i > 0) { + final char c1 = result.charAt(i - 1); + if (c1 == '\r' || c1 == '\n') { + if (++versionTimes == 4) break; + } + } + + stringBuilder.append(c); + } + + final String strReadMore = resources.getString(R.string.read_more); + stringBuilder.append('\n').append(strReadMore); + + final int sbLen = stringBuilder.length(); + stringBuilder.setSpan(new URLSpan("https://gitlab.com/AwaisKing/instagrabber/-/blob/master/CHANGELOG"), + sbLen - strReadMore.length(), sbLen, 0); + + textView.setText(stringBuilder, TextView.BufferType.SPANNABLE); + + alertDialog.show(); + } + + settingsHelper.putInteger(Constants.PREV_INSTALL_VERSION, BuildConfig.VERSION_CODE); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/LocaleUtils.java b/app/src/main/java/awais/instagrabber/utils/LocaleUtils.java new file mode 100755 index 00000000..ad575bce --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/LocaleUtils.java @@ -0,0 +1,70 @@ +package awais.instagrabber.utils; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.view.ContextThemeWrapper; + +import androidx.annotation.Nullable; + +import java.util.Locale; + +// taken from my app TESV Console Codes +public final class LocaleUtils { + private static Locale defaultLocale, currentLocale; + + public static void setLocale(Context baseContext) { + if (defaultLocale == null) defaultLocale = Locale.getDefault(); + + if (baseContext instanceof ContextThemeWrapper) + baseContext = ((ContextThemeWrapper) baseContext).getBaseContext(); + + final String lang = LocaleUtils.getCorrespondingLanguageCode(baseContext); + + currentLocale = Utils.isEmpty(lang) ? defaultLocale : new Locale(lang); + Locale.setDefault(currentLocale); + + final Resources res = baseContext.getResources(); + final Configuration config = res.getConfiguration(); + + config.locale = currentLocale; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + config.setLocale(currentLocale); + config.setLayoutDirection(currentLocale); + } + + res.updateConfiguration(config, res.getDisplayMetrics()); + } + + public static Locale getCurrentLocale() { + return currentLocale; + } + + public static void updateConfig(final ContextThemeWrapper wrapper) { + if (currentLocale != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + final Configuration configuration = new Configuration(); + configuration.locale = currentLocale; + configuration.setLocale(currentLocale); + wrapper.applyOverrideConfiguration(configuration); + } + } + + @Nullable + private static String getCorrespondingLanguageCode(final Context baseContext) { + if (Utils.settingsHelper == null) + Utils.settingsHelper = new SettingsHelper(baseContext); + + final int appLanguageIndex = Utils.settingsHelper.getInteger(Constants.APP_LANGUAGE); + + // todo keep adding languages till i die...... or find a big tiddy goth gf ;-; + if (appLanguageIndex == 1) return "en"; + if (appLanguageIndex == 2) return "fr"; + if (appLanguageIndex == 3) return "es"; + if (appLanguageIndex == 4) return "zh"; + if (appLanguageIndex == 5) return "in"; + if (appLanguageIndex == 6) return "it"; + + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/MyApps.java b/app/src/main/java/awais/instagrabber/utils/MyApps.java new file mode 100755 index 00000000..5d39568d --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/MyApps.java @@ -0,0 +1,138 @@ +package awais.instagrabber.utils; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Build; +import android.os.Process; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.appcompat.widget.AppCompatTextView; + +import java.util.Random; + +import awais.instagrabber.R; + +public final class MyApps { + public final static Icons[] iconsList = Icons.values(); + + @SuppressWarnings("unused") + public enum Icons { + MEDIASCAN("awais.media.scanner", "mediaScanner", R.drawable.zzz_ms), + ADDME("awais.addme", "AddMe", R.drawable.zzz_adm), + LINKEDWORDS("awais.backworddictionary", "Linked Words", R.drawable.zzz_lw), + QUODB("awais.quodb", "QuoDB", R.drawable.zzz_qdb), + REVERSIFY("awais.reversify", "Reversify", R.drawable.zzz_rev), + REVERSIFY_LITE("awais.reversify.lite", "Reversify Lite", R.drawable.zzz_revl), + TESV("awais.skyrimconsole", "Skyrim Cheats", R.drawable.zzz_tesv); + private final int icon; + private final String name, pkg; + + Icons(final String pkg, final String name, final int icon) { + this.name = name; + this.pkg = pkg; + this.icon = icon; + } + } + + public static void openAppStore(@NonNull final Context context, final int position) { + context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + MyApps.iconsList[position].pkg))); + } + + public static void showAlertDialog(final Context context, final AdapterView.OnItemClickListener clickListener) { + final DialogInterface.OnCancelListener cancelListener = d -> { + if (clickListener != null) clickListener.onItemClick(null, null, -1, -1); + else Process.killProcess(Process.myPid()); + }; + if (new Random().nextDouble() < 0.420D) { + cancelListener.onCancel(null); + return; + } + final GridView gridView = new GridView(context); + gridView.setAdapter(new ImageAdapter(context)); + gridView.setNumColumns(3); + gridView.setOnItemClickListener(clickListener); + final AlertDialog dialog = new AlertDialog.Builder(context).setView(gridView).setTitle("Support my apps tho").create(); + dialog.setOnCancelListener(cancelListener); + dialog.show(); + } + + public static class ImageAdapter extends BaseAdapter { + private final Context context; + private final int size; + + public ImageAdapter(final Context context) { + this.context = context; + this.size = (int) (80 * Resources.getSystem().getDisplayMetrics().density); + } + + @Override + public int getCount() { + return iconsList.length; + } + + @Override + public Object getItem(final int position) { + return iconsList[position]; + } + + @Override + public long getItemId(final int position) { + return 0; + } + + public View getView(final int position, View convertView, final ViewGroup parent) { + final ViewHolder holder; + if (convertView == null) { + final LinearLayout linearLayout = new LinearLayout(context); + linearLayout.setOrientation(LinearLayout.VERTICAL); + final AppCompatImageView imageView = new AppCompatImageView(context); + final AppCompatTextView textView = new AppCompatTextView(context); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + textView.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY); + textView.setGravity(Gravity.CENTER_HORIZONTAL); + imageView.setAdjustViewBounds(true); + linearLayout.addView(imageView, LinearLayout.LayoutParams.MATCH_PARENT, size); + linearLayout.addView(textView); + final int padding = size >> 2; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + linearLayout.setPaddingRelative(padding, padding, padding, padding); + else linearLayout.setPadding(padding, padding, padding, padding); + convertView = linearLayout; + convertView.setTag(holder = new ViewHolder(textView, imageView)); + } else + holder = (ViewHolder) convertView.getTag(); + + final Object item = getItem(position); + if (item instanceof Icons) { + final Icons icons = (Icons) item; + holder.title.setText(icons.name); + holder.icon.setImageResource(icons.icon); + } + return convertView; + } + + private final static class ViewHolder { + private final TextView title; + private final ImageView icon; + + private ViewHolder(final TextView title, final ImageView icon) { + this.title = title; + this.icon = icon; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java new file mode 100755 index 00000000..2a09d438 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -0,0 +1,115 @@ +package awais.instagrabber.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.StringDef; +import androidx.appcompat.app.AppCompatDelegate; + +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.COOKIE; +import static awais.instagrabber.utils.Constants.CUSTOM_DATE_TIME_FORMAT; +import static awais.instagrabber.utils.Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED; +import static awais.instagrabber.utils.Constants.DATE_TIME_FORMAT; +import static awais.instagrabber.utils.Constants.DATE_TIME_SELECTION; +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.PREV_INSTALL_VERSION; +import static awais.instagrabber.utils.Constants.PROFILE_FETCH_MODE; +import static awais.instagrabber.utils.Constants.SHOW_FEED; +import static awais.instagrabber.utils.Constants.SHOW_QUICK_ACCESS_DIALOG; + +public final class SettingsHelper { + private final SharedPreferences sharedPreferences; + + public SettingsHelper(@NonNull final Context context) { + this.sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE); + } + + @NonNull + public String getString(@StringSettings final String key) { + final String stringDefault = getStringDefault(key); + if (sharedPreferences != null) return sharedPreferences.getString(key, stringDefault); + return stringDefault; + } + + public int getInteger(@IntegerSettings final String key) { + final int integerDefault = getIntegerDefault(key); + if (sharedPreferences != null) return sharedPreferences.getInt(key, integerDefault); + return integerDefault; + } + + public boolean getBoolean(@BooleanSettings final String key) { + final boolean booleanDefault = getBooleanDefault(key); + if (sharedPreferences != null) return sharedPreferences.getBoolean(key, booleanDefault); + return booleanDefault; + } + + @NonNull + private String getStringDefault(@StringSettings final String key) { + if (DATE_TIME_FORMAT.equals(key)) + return "hh:mm:ss a 'on' dd-MM-yyyy"; + if (DATE_TIME_SELECTION.equals(key)) + return "0;3;0"; + return ""; + } + + private int getIntegerDefault(@IntegerSettings final String key) { + if (APP_THEME.equals(key)) return getThemeCode(true); + if (PREV_INSTALL_VERSION.equals(key)) return -1; + return 0; + } + + private boolean getBooleanDefault(@BooleanSettings final String key) { + return BOTTOM_TOOLBAR.equals(key) || + AUTOPLAY_VIDEOS.equals(key) || + SHOW_QUICK_ACCESS_DIALOG.equals(key) || + MUTED_VIDEOS.equals(key); + } + + public int getThemeCode(final boolean fromHelper) { + int themeCode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; + + if (!fromHelper && sharedPreferences != null) { + themeCode = sharedPreferences.getInt(APP_THEME, themeCode); + if (themeCode == 1) themeCode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; + else if (themeCode == 3) themeCode = AppCompatDelegate.MODE_NIGHT_NO; + else if (themeCode != 2) themeCode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; + } + + if (themeCode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM && Build.VERSION.SDK_INT < 29) + themeCode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; + + return themeCode; + } + + public void putString(@StringSettings final String key, final String val) { + if (sharedPreferences != null) sharedPreferences.edit().putString(key, val).apply(); + } + + public void putInteger(@IntegerSettings final String key, final int val) { + if (sharedPreferences != null) sharedPreferences.edit().putInt(key, val).apply(); + } + + public void putBoolean(@BooleanSettings final String key, final boolean val) { + if (sharedPreferences != null) sharedPreferences.edit().putBoolean(key, val).apply(); + } + + @StringDef({COOKIE, FOLDER_PATH, DATE_TIME_FORMAT, DATE_TIME_SELECTION, CUSTOM_DATE_TIME_FORMAT}) + public @interface StringSettings {} + + @StringDef({DOWNLOAD_USER_FOLDER, BOTTOM_TOOLBAR, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS, + AUTOLOAD_POSTS, SHOW_FEED, CUSTOM_DATE_TIME_FORMAT_ENABLED}) + public @interface BooleanSettings {} + + @StringDef({APP_THEME, APP_LANGUAGE, PROFILE_FETCH_MODE, PREV_INSTALL_VERSION}) + public @interface IntegerSettings {} +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/UpdateChecker.java b/app/src/main/java/awais/instagrabber/utils/UpdateChecker.java new file mode 100755 index 00000000..0605af07 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/UpdateChecker.java @@ -0,0 +1,75 @@ +package awais.instagrabber.utils; + +import android.os.AsyncTask; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.net.HttpURLConnection; +import java.net.URL; + +import awais.instagrabber.BuildConfig; +import awais.instagrabber.interfaces.FetchListener; + +public final class UpdateChecker extends AsyncTask { + private final FetchListener fetchListener; + private String versionUrl; + + public UpdateChecker(final FetchListener fetchListener) { + this.fetchListener = fetchListener; + } + + @NonNull + @Override + protected Boolean doInBackground(final Void... voids) { + final String UPDATE_BASE_URL = "https://gitlab.com/AwaisKing/instagrabber/-/releases/v"; + final String versionName = BuildConfig.VERSION_NAME; + final int index = versionName.indexOf('.'); + + try { + final int verMajor = Integer.parseInt(versionName.substring(0, index)); + + versionUrl = UPDATE_BASE_URL + (verMajor + 1) + ".0"; + + // check major version first + HttpURLConnection conn = (HttpURLConnection) new URL(versionUrl).openConnection(); + conn.setUseCaches(false); + conn.setRequestMethod("HEAD"); + conn.connect(); + + final int responseCode = conn.getResponseCode(); + conn.disconnect(); + + if (responseCode == HttpURLConnection.HTTP_OK) return true; + else { + final String substring = versionName.substring(index + 1); + final int verMinor = Integer.parseInt(substring) + 1; + + for (int i = verMinor; i < 10; ++i) { + versionUrl = UPDATE_BASE_URL + verMajor + '.' + i; + conn.disconnect(); + + conn = (HttpURLConnection) new URL(versionUrl).openConnection(); + conn.setUseCaches(false); + conn.setRequestMethod("HEAD"); + conn.connect(); + + if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { + conn.disconnect(); + return true; + } + } + } + } catch (final Exception e) { + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + + return false; + } + + @Override + protected void onPostExecute(final Boolean result) { + if (result != null && result && fetchListener != null) + fetchListener.onResult(versionUrl); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/UrlEncoder.java b/app/src/main/java/awais/instagrabber/utils/UrlEncoder.java new file mode 100755 index 00000000..4795ad97 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/UrlEncoder.java @@ -0,0 +1,74 @@ +package awais.instagrabber.utils; + +import androidx.annotation.NonNull; + +import java.io.CharArrayWriter; +import java.util.BitSet; + +// same as java.net.URLEncoder +public final class UrlEncoder { + private static final BitSet dontNeedEncoding = new BitSet(256); + private static final int caseDiff = ('a' - 'A'); + + static { + int i; + for (i = 'a'; i <= 'z'; i++) dontNeedEncoding.set(i); + for (i = 'A'; i <= 'Z'; i++) dontNeedEncoding.set(i); + for (i = '0'; i <= '9'; i++) dontNeedEncoding.set(i); + dontNeedEncoding.set(' '); + dontNeedEncoding.set('-'); + dontNeedEncoding.set('_'); + dontNeedEncoding.set('.'); + dontNeedEncoding.set('*'); + } + + @NonNull + public static String encodeUrl(@NonNull final String s) { + final StringBuilder out = new StringBuilder(s.length()); + final CharArrayWriter charArrayWriter = new CharArrayWriter(); + + boolean needToChange = false; + for (int i = 0; i < s.length(); ) { + int c = s.charAt(i); + + if (dontNeedEncoding.get(c)) { + if (c == ' ') { + c = '+'; + needToChange = true; + } + + out.append((char) c); + i++; + } else { + do { + charArrayWriter.write(c); + if (c >= 0xD800 && c <= 0xDBFF && i + 1 < s.length()) { + final int d = s.charAt(i + 1); + if (d >= 0xDC00 && d <= 0xDFFF) { + charArrayWriter.write(d); + i++; + } + } + i++; + } while (i < s.length() && !dontNeedEncoding.get(c = s.charAt(i))); + + charArrayWriter.flush(); + + final byte[] ba = charArrayWriter.toString().getBytes(); + for (final byte b : ba) { + out.append('%'); + char ch = Character.forDigit((b >> 4) & 0xF, 16); + if (Character.isLetter(ch)) ch -= caseDiff; + out.append(ch); + ch = Character.forDigit(b & 0xF, 16); + if (Character.isLetter(ch)) ch -= caseDiff; + out.append(ch); + } + charArrayWriter.reset(); + needToChange = true; + } + } + + return (needToChange ? out.toString() : s); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java new file mode 100755 index 00000000..83ec6f5d --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -0,0 +1,1270 @@ +package awais.instagrabber.utils; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageInfo; +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.Environment; +import android.text.Editable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.RelativeSizeSpan; +import android.text.style.URLSpan; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.CookieManager; +import android.webkit.MimeTypeMap; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentManager; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FilenameFilter; +import java.io.InputStreamReader; +import java.net.CookiePolicy; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +import awais.instagrabber.BuildConfig; +import awais.instagrabber.R; +import awais.instagrabber.activities.Main; +import awais.instagrabber.asyncs.DownloadAsync; +import awais.instagrabber.asyncs.PostFetcher; +import awais.instagrabber.customviews.CommentMentionClickSpan; +import awais.instagrabber.databinding.DialogImportExportBinding; +import awais.instagrabber.models.BasePostModel; +import awais.instagrabber.models.FeedStoryModel; +import awais.instagrabber.models.HighlightModel; +import awais.instagrabber.models.IntentModel; +import awais.instagrabber.models.ProfileModel; +import awais.instagrabber.models.StoryModel; +import awais.instagrabber.models.direct_messages.DirectItemModel; +import awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemRavenMediaModel; +import awais.instagrabber.models.direct_messages.InboxThreadModel; +import awais.instagrabber.models.enums.DirectItemType; +import awais.instagrabber.models.enums.DownloadMethod; +import awais.instagrabber.models.enums.InboxReadState; +import awais.instagrabber.models.enums.IntentModelType; +import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.models.enums.RavenExpiringMediaType; +import awais.instagrabber.models.enums.RavenMediaViewType; +import awaisomereport.LogCollector; + +import static awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemActionLogModel; +import static awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemAnimatedMediaModel; +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.DirectItemMediaModel; +import static awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemReelShareModel; +import static awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemVideoCallEventModel; +import static awais.instagrabber.models.direct_messages.DirectItemModel.DirectItemVoiceMediaModel; +import static awais.instagrabber.models.direct_messages.DirectItemModel.RavenExpiringMediaActionSummaryModel; +import static awais.instagrabber.utils.Constants.FOLDER_PATH; +import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; + +public final class Utils { + public static LogCollector logCollector; + public static SettingsHelper settingsHelper; + public static DataBox dataBox; + public static boolean sessionVolumeFull = false; + @SuppressLint("StaticFieldLeak") + public static NotificationManagerCompat notificationManager; + public static final CookieManager COOKIE_MANAGER = CookieManager.getInstance(); + public static final String[] PERMS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; + public static final java.net.CookieManager NET_COOKIE_MANAGER = new java.net.CookieManager(null, CookiePolicy.ACCEPT_ALL); + public static final MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + public static final String CHANNEL_ID = "InstaGrabber", CHANNEL_NAME = "Instagrabber", + NOTIF_GROUP_NAME = "awais.instagrabber.InstaNotif"; + public static boolean isChannelCreated = false; + public static boolean isInstagramInstalled = false; + public static String telegramPackage; + public static ClipboardManager clipboardManager; + public static DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics(); + public static SimpleDateFormat datetimeParser; + + public static void setupCookies(final String cookieRaw) { + if (cookieRaw != null && !isEmpty(cookieRaw)) { + final CookieStore cookieStore = NET_COOKIE_MANAGER.getCookieStore(); + try { + final URI uri1 = new URI("https://instagram.com"); + final URI uri2 = new URI("https://instagram.com/"); + for (final String cookie : cookieRaw.split(";")) { + final String[] strings = cookie.split("=", 2); + final HttpCookie httpCookie = new HttpCookie(strings[0].trim(), strings[1].trim()); + httpCookie.setDomain("instagram.com"); + httpCookie.setPath("/"); + httpCookie.setVersion(0); + cookieStore.add(uri1, httpCookie); + cookieStore.add(uri2, httpCookie); + } + } catch (final URISyntaxException e) { + if (logCollector != null) + logCollector.appendException(e, LogCollector.LogFile.UTILS, "setupCookies"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } + } + + @Nullable + public static String getUserIdFromCookie(final String cookie) { + if (!isEmpty(cookie)) { + final int uidIndex = cookie.indexOf("ds_user_id"); + if (uidIndex > 0) { + final int uidEndIndex = cookie.indexOf(';', uidIndex + 10); + if (uidEndIndex > 0) { + final String uid = cookie.substring(uidIndex + 11, uidEndIndex); + return !isEmpty(uid) ? uid : null; + } + } + } + return null; + } + + @Nullable + public static IntentModel stripString(@NonNull String clipString) { + final int wwwDel = clipString.contains("www.") ? 4 : 0; + final boolean isHttps = clipString.startsWith("https"); + + IntentModelType type = IntentModelType.UNKNOWN; + if (clipString.contains("instagram.com/")) { + clipString = clipString.substring((isHttps ? 22 : 21) + wwwDel); + + final char firstChar = clipString.charAt(0); + if ((firstChar == 'p' || firstChar == 'P') && clipString.charAt(1) == '/') { + clipString = clipString.substring(2); + type = IntentModelType.POST; + } else if (clipString.startsWith("explore/tags/")) { + clipString = clipString.substring(13); + type = IntentModelType.HASHTAG; + } + + clipString = cleanString(clipString); + } else if (clipString.contains("ig.me/u/")) { + clipString = clipString.substring((isHttps ? 16 : 15) + wwwDel); + clipString = cleanString(clipString); + type = IntentModelType.USERNAME; + + } else return null; + + final int clipLen = clipString.length() - 1; + if (clipString.charAt(clipLen) == '/') + clipString = clipString.substring(0, clipLen); + + return new IntentModel(type, clipString); + } + + @NonNull + public static String cleanString(@NonNull final String clipString) { + final int queryIndex = clipString.indexOf('?'); + final int paramIndex = clipString.indexOf('#'); + int startIndex = -1; + if (queryIndex > 0 && paramIndex > 0) { + if (queryIndex < paramIndex) startIndex = queryIndex; + else if (paramIndex < queryIndex) startIndex = paramIndex; + } else if (queryIndex == -1 && paramIndex > 0) startIndex = paramIndex; + else if (paramIndex == -1 && queryIndex > 0) startIndex = queryIndex; + return startIndex != -1 ? clipString.substring(0, startIndex) : clipString; + } + + @NonNull + public static CharSequence getMentionText(@NonNull final CharSequence text) { + final int commentLength = text.length(); + final SpannableStringBuilder stringBuilder = new SpannableStringBuilder(text, 0, commentLength); + + for (int i = 0; i < commentLength; ++i) { + char currChar = text.charAt(i); + + if (currChar == '@' || currChar == '#') { + final int startLen = i; + + do { + if (++i == commentLength) break; + currChar = text.charAt(i); + + if (currChar == '.') { + final char nextChar = text.charAt(i + 1); + if (nextChar == '.' || nextChar == ' ' || nextChar == '#' || nextChar == '@' || nextChar == '/' + || nextChar == '\r' || nextChar == '\n') { + break; + } + } + + // for merged hashtags + if (currChar == '#') { + --i; + break; + } + } while (currChar != ' ' && currChar != '\r' && currChar != '\n' && currChar != '>' && currChar != '<' + && currChar != ':' && currChar != ';' && currChar != '\'' && currChar != '"' && currChar != '[' + && currChar != ']' && currChar != '\\' && currChar != '=' && currChar != '-' && currChar != '!' + && currChar != '$' && currChar != '%' && currChar != '^' && currChar != '&' && currChar != '*' + && currChar != '(' && currChar != ')' && currChar != '{' && currChar != '}' && currChar != '/' + && currChar != '|' && currChar != '?' && currChar != '`' && currChar != '~'); + + final int endLen = currChar != '#' ? i : i + 1; // for merged hashtags + stringBuilder.setSpan(new CommentMentionClickSpan(), startLen, + Math.min(commentLength, endLen), // fixed - crash when end index is greater than comment length ( @kernoeb ) + Spanned.SPAN_EXCLUSIVE_INCLUSIVE); + } + } + + return stringBuilder; + } + + @Nullable + public static String getHighQualityPost(final JSONArray resources, final boolean isVideo) { + try { + final int resourcesLen = resources.length(); + + final String[] sources = new String[resourcesLen]; + int lastResMain = 0, lastIndexMain = -1; + int lastResBase = 0, lastIndexBase = -1; + for (int i = 0; i < resourcesLen; ++i) { + final JSONObject item = resources.getJSONObject(i); + if (item != null && (!isVideo || item.has(Constants.EXTRAS_PROFILE))) { + sources[i] = item.getString("src"); + final int currRes = item.getInt("config_width") * item.getInt("config_height"); + + final String profile = isVideo ? item.getString(Constants.EXTRAS_PROFILE) : null; + + if (!isVideo || "MAIN".equals(profile)) { + if (currRes > lastResMain) { + lastResMain = currRes; + lastIndexMain = i; + } + } else if ("BASELINE".equals(profile)) { + if (currRes > lastResBase) { + lastResBase = currRes; + lastIndexBase = i; + } + } + } + } + + if (lastIndexMain >= 0) return sources[lastIndexMain]; + else if (lastIndexBase >= 0) return sources[lastIndexBase]; + } catch (final Exception e) { + if (logCollector != null) + logCollector.appendException(e, LogCollector.LogFile.UTILS, "getHighQualityPost", + new Pair<>("resourcesNull", resources == null), + new Pair<>("isVideo", isVideo)); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + return null; + } + + public static String getHighQualityImage(final JSONObject resources) { + String src = null; + try { + if (resources.has("display_resources")) src = getHighQualityPost(resources.getJSONArray("display_resources"), false); + if (src == null) return resources.getString("display_url"); + } catch (final Exception e) { + if (logCollector != null) + logCollector.appendException(e, LogCollector.LogFile.UTILS, "getHighQualityImage", + new Pair<>("resourcesNull", resources == null)); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + return src; + } + + public static String getItemThumbnail(@NonNull final JSONArray jsonArray) { + String thumbnail = null; + final int imageResLen = jsonArray.length(); + + for (int i = 0; i < imageResLen; ++i) { + final JSONObject imageResource = jsonArray.optJSONObject(i); + try { + final int width = imageResource.getInt("width"); + final int height = imageResource.getInt("height"); + final float ratio = Float.parseFloat(String.format(Locale.ENGLISH, "%.2f", (float) height / width)); + if (ratio >= 0.95f && ratio <= 1.0f) { + thumbnail = imageResource.getString("url"); + break; + } + } catch (final Exception e) { + if (logCollector != null) + logCollector.appendException(e, LogCollector.LogFile.UTILS, "getItemThumbnail"); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + thumbnail = null; + } + } + + if (Utils.isEmpty(thumbnail)) thumbnail = jsonArray.optJSONObject(0).optString("url"); + + return thumbnail; + } + + @Nullable + public static String getThumbnailUrl(@NonNull final JSONObject mediaObj, final MediaItemType mediaType) throws Exception { + String thumbnail = null; + + if (mediaType == MediaItemType.MEDIA_TYPE_IMAGE || mediaType == MediaItemType.MEDIA_TYPE_VIDEO) { + final JSONObject imageVersions = mediaObj.optJSONObject("image_versions2"); + if (imageVersions != null) + thumbnail = Utils.getItemThumbnail(imageVersions.getJSONArray("candidates")); + + } else if (mediaType == MediaItemType.MEDIA_TYPE_SLIDER) { + final JSONArray carouselMedia = mediaObj.optJSONArray("carousel_media"); + if (carouselMedia != null) thumbnail = Utils.getItemThumbnail(carouselMedia.getJSONObject(0) + .getJSONObject("image_versions2").getJSONArray("candidates")); + } + + return thumbnail; + } + + @Nullable + public static MediaItemType getMediaItemType(final int mediaType) { + if (mediaType == 1) return MediaItemType.MEDIA_TYPE_IMAGE; + if (mediaType == 2) return MediaItemType.MEDIA_TYPE_VIDEO; + if (mediaType == 8) return MediaItemType.MEDIA_TYPE_SLIDER; + if (mediaType == 11) return MediaItemType.MEDIA_TYPE_VOICE; + return null; + } + + public static DirectItemMediaModel getDirectMediaModel(final JSONObject mediaObj) throws Exception { + final DirectItemMediaModel mediaModel; + if (mediaObj == null) mediaModel = null; + else { + final JSONObject userObj = mediaObj.optJSONObject("user"); + + ProfileModel user = null; + if (userObj != null) { + user = new ProfileModel( + userObj.getBoolean("is_private"), + userObj.optBoolean("is_verified"), + String.valueOf(userObj.get("pk")), + userObj.getString("username"), + userObj.getString("full_name"), + null, null, + userObj.getString("profile_pic_url"), + null, 0, 0, 0); + } + + final MediaItemType mediaType = getMediaItemType(mediaObj.optInt("media_type", -1)); + + String id = mediaObj.optString("id"); + if (Utils.isEmpty(id)) id = null; + + mediaModel = new DirectItemMediaModel(mediaType, + mediaObj.optLong("expiring_at"), + mediaObj.optLong("pk"), + id, + getThumbnailUrl(mediaObj, mediaType), + user); + } + return mediaModel; + } + + private static DirectItemType getDirectItemType(final String itemType) { + if ("placeholder".equals(itemType)) return DirectItemType.PLACEHOLDER; + if ("media".equals(itemType)) return DirectItemType.MEDIA; + if ("link".equals(itemType)) return DirectItemType.LINK; + if ("like".equals(itemType)) return DirectItemType.LIKE; + if ("reel_share".equals(itemType)) return DirectItemType.REEL_SHARE; + if ("media_share".equals(itemType)) return DirectItemType.MEDIA_SHARE; + if ("action_log".equals(itemType)) return DirectItemType.ACTION_LOG; + if ("raven_media".equals(itemType)) return DirectItemType.RAVEN_MEDIA; + if ("profile".equals(itemType)) return DirectItemType.PROFILE; + if ("video_call_event".equals(itemType)) return DirectItemType.VIDEO_CALL_EVENT; + if ("animated_media".equals(itemType)) return DirectItemType.ANIMATED_MEDIA; + if ("voice_media".equals(itemType)) return DirectItemType.VOICE_MEDIA; + //if ("story_share".equals(itemType)) return DirectItemType.STORY_SHARE; + return DirectItemType.TEXT; + } + + @NonNull + public static InboxThreadModel createInboxThreadModel(@NonNull final JSONObject data, final boolean inThreadView) throws Exception { + final InboxReadState readState = data.getInt("read_state") == 0 ? InboxReadState.STATE_READ : InboxReadState.STATE_UNREAD; + final String threadType = data.getString("thread_type");// private = dms, [??] = group + + final String threadId = data.getString("thread_id"); + final String threadV2Id = data.getString("thread_v2_id"); + final String threadTitle = data.getString("thread_title"); + + final String threadNewestCursor = data.getString("newest_cursor"); + final String threadOldestCursor = data.getString("oldest_cursor"); + final String threadNextCursor = data.has("next_cursor") ? data.getString("next_cursor") : null; + final String threadPrevCursor = data.has("prev_cursor") ? data.getString("prev_cursor") : null; + + final boolean threadHasOlder = data.getBoolean("has_older"); + final boolean threadHasNewer = data.getBoolean("has_newer"); + + final long lastActivityAt = data.optLong("last_activity_at"); + final boolean named = data.optBoolean("named"); + final boolean muted = data.optBoolean("muted"); + final boolean isPin = data.optBoolean("is_pin"); + final boolean isSpam = data.optBoolean("is_spam"); + final boolean isGroup = data.optBoolean("is_group"); + final boolean pending = data.optBoolean("pending"); + final boolean archived = data.optBoolean("archived"); + final boolean canonical = data.optBoolean("canonical"); + + final JSONArray users = data.getJSONArray("users"); + final int usersLen = users.length(); + + final ProfileModel[] userModels = new ProfileModel[usersLen]; + for (int j = 0; j < usersLen; ++j) { + final JSONObject userObject = users.getJSONObject(j); + userModels[j] = new ProfileModel(userObject.getBoolean("is_private"), + userObject.optBoolean("is_verified"), + String.valueOf(userObject.get("pk")), + userObject.getString("username"), + userObject.getString("full_name"), + null, null, + userObject.getString("profile_pic_url"), + null, 0, 0, 0); + } + + final JSONArray items = data.getJSONArray("items"); + final int itemsLen = items.length(); + + final ArrayList itemModels = new ArrayList<>(itemsLen); + for (int i = 0; i < itemsLen; ++i) { + final JSONObject itemObject = items.getJSONObject(i); + + CharSequence text = null; + ProfileModel profileModel = null; + DirectItemLinkModel linkModel = null; + DirectItemMediaModel directMedia = null; + DirectItemReelShareModel reelShareModel = null; + DirectItemActionLogModel actionLogModel = null; + DirectItemAnimatedMediaModel animatedMediaModel = null; + DirectItemVoiceMediaModel voiceMediaModel = null; + DirectItemRavenMediaModel ravenMediaModel = null; + DirectItemVideoCallEventModel videoCallEventModel = null; + + final DirectItemType itemType = getDirectItemType(itemObject.getString("item_type")); + switch (itemType) { + case ANIMATED_MEDIA: { + final JSONObject animatedMedia = itemObject.getJSONObject("animated_media"); + final JSONObject stickerImage = animatedMedia.getJSONObject("images").getJSONObject("fixed_height"); + + animatedMediaModel = new DirectItemAnimatedMediaModel(animatedMedia.getBoolean("is_random"), + animatedMedia.getBoolean("is_sticker"), animatedMedia.getString("id"), + stickerImage.getString("url"), stickerImage.optString("webp"), stickerImage.optString("mp4"), + stickerImage.getInt("height"), stickerImage.getInt("width")); + } + break; + + case VOICE_MEDIA: { + final JSONObject voiceMedia = itemObject.getJSONObject("voice_media").getJSONObject("media"); + final JSONObject audio = voiceMedia.getJSONObject("audio"); + + int[] waveformData = null; + final JSONArray waveformDataArray = audio.optJSONArray("waveform_data"); + if (waveformDataArray != null) { + final int waveformDataLen = waveformDataArray.length(); + waveformData = new int[waveformDataLen]; + // 0.011775206 + for (int j = 0; j < waveformDataLen; ++j) { + waveformData[j] = (int) (waveformDataArray.optDouble(j) * 10); + } + } + + voiceMediaModel = new DirectItemVoiceMediaModel(voiceMedia.getString("id"), + audio.getString("audio_src"), audio.getLong("duration"), + waveformData); + } + break; + + case LINK: { + final JSONObject linkObj = itemObject.getJSONObject("link"); + + DirectItemLinkContext itemLinkContext = null; + final JSONObject linkContext = linkObj.optJSONObject("link_context"); + if (linkContext != null) { + itemLinkContext = new DirectItemLinkContext( + linkContext.getString("link_url"), + linkContext.optString("link_title"), + linkContext.optString("link_summary"), + linkContext.optString("link_image_url") + ); + } + + linkModel = new DirectItemLinkModel(linkObj.getString("text"), + linkObj.getString("client_context"), + linkObj.getString("mutation_token"), + itemLinkContext); + } + break; + + case REEL_SHARE: { + final JSONObject reelShare = itemObject.getJSONObject("reel_share"); + Log.d("AWAISKING_APP", "(rs) itemObject: " + itemObject); // todo + reelShareModel = new DirectItemReelShareModel( + reelShare.optBoolean("is_reel_persisted"), + reelShare.getLong("reel_owner_id"), + reelShare.getString("text"), + reelShare.getString("type"), + reelShare.getString("reel_type"), + reelShare.optString("reel_name"), + reelShare.optString("reel_id"), + getDirectMediaModel(reelShare.optJSONObject("media"))); + } + break; + + case RAVEN_MEDIA: { + final JSONObject visualMedia = itemObject.getJSONObject("visual_media"); + + final JSONArray seenUserIdsArray = visualMedia.getJSONArray("seen_user_ids"); + final int seenUsersLen = seenUserIdsArray.length(); + final String[] seenUserIds = new String[seenUsersLen]; + for (int j = 0; j < seenUsersLen; j++) seenUserIds[j] = seenUserIdsArray.getString(j); + + RavenExpiringMediaActionSummaryModel expiringSummaryModel = null; + final JSONObject actionSummary = visualMedia.optJSONObject("expiring_media_action_summary"); + if (actionSummary != null) expiringSummaryModel = new RavenExpiringMediaActionSummaryModel( + actionSummary.getLong("timestamp"), actionSummary.getInt("count"), + getExpiringMediaType(actionSummary.getString("type"))); + + final RavenMediaViewType viewType; + final String viewMode = visualMedia.getString("view_mode"); + switch (viewMode) { + case "replayable": + viewType = RavenMediaViewType.REPLAYABLE; + break; + case "permanent": + viewType = RavenMediaViewType.PERMANENT; + break; + case "once": + default: + viewType = RavenMediaViewType.ONCE; + } + + ravenMediaModel = new DirectItemRavenMediaModel( + visualMedia.getLong(viewType == RavenMediaViewType.PERMANENT ? "url_expire_at_secs" : "replay_expiring_at_us"), + visualMedia.optInt("playback_duration_secs"), + visualMedia.getInt("seen_count"), + seenUserIds, + viewType, + getDirectMediaModel(visualMedia.optJSONObject("media")), + expiringSummaryModel); + + } + break; + + case VIDEO_CALL_EVENT: { + final JSONObject videoCallEvent = itemObject.getJSONObject("video_call_event"); + videoCallEventModel = new DirectItemVideoCallEventModel(videoCallEvent.getLong("vc_id"), + videoCallEvent.optBoolean("thread_has_audio_only_call"), + videoCallEvent.getString("action"), + videoCallEvent.getString("description")); + } + break; + + case PROFILE: { + final JSONObject profile = itemObject.getJSONObject("profile"); + profileModel = new ProfileModel(profile.getBoolean("is_private"), + profile.getBoolean("is_verified"), + Long.toString(profile.getLong("pk")), + profile.getString("username"), + profile.getString("full_name"), + null, null, + profile.getString("profile_pic_url"), + null, 0, 0, 0); + } + break; + + case PLACEHOLDER: { + final JSONObject placeholder = itemObject.getJSONObject("placeholder"); + + final String title = placeholder.getString("title"); + final String message = placeholder.getString("message"); + + final SpannableString spannableString = new SpannableString(title + '\n' + message); + spannableString.setSpan(new RelativeSizeSpan(1.15f), 0, title.length(), 0); + + text = hasMentions(message) ? getMentionText(spannableString) : spannableString; + } + break; + + case ACTION_LOG: + if (inThreadView && itemObject.optInt("hide_in_thread", 0) != 0) + // prevents empty viewholders when in thread view mode + continue; + final JSONObject actionLog = itemObject.getJSONObject("action_log"); + actionLogModel = new DirectItemActionLogModel(actionLog.getString("description") + // todo add bold , text_attributes objects [find out how tf to implement them] + ); + break; + + case MEDIA_SHARE: + directMedia = getDirectMediaModel(itemObject.getJSONObject("media_share")); + break; + + case MEDIA: + directMedia = getDirectMediaModel(itemObject.optJSONObject("media")); + break; + + case LIKE: + text = new SpannableString(itemObject.getString("like")); + ((SpannableString) text).setSpan(new RelativeSizeSpan(15f), 0, text.length(), 0); + break; + + /*case STORY_SHARE: + if*/ + + case TEXT: + if (!itemObject.has("text")) + Log.d("AWAISKING_APP", "itemObject: " + itemObject); // todo + text = itemObject.optString("text"); + break; + } + + itemModels.add(new DirectItemModel( + itemObject.getLong("user_id"), + itemObject.getLong("timestamp"), + itemObject.getString("item_id"), + itemType, + text, + linkModel, + profileModel, + reelShareModel, + directMedia, + actionLogModel, + voiceMediaModel, + ravenMediaModel, + videoCallEventModel, + animatedMediaModel)); + } + + itemModels.trimToSize(); + + return new InboxThreadModel(readState, threadId, threadV2Id, threadType, threadTitle, + threadNewestCursor, threadOldestCursor, threadNextCursor, threadPrevCursor, + null, // todo + userModels, + null, // todo + itemModels.toArray(new DirectItemModel[0]), + muted, isPin, named, canonical, + pending, threadHasOlder, threadHasNewer, isSpam, isGroup, archived, lastActivityAt); + } + + private static RavenExpiringMediaType getExpiringMediaType(final String type) { + if ("raven_sent".equals(type)) return RavenExpiringMediaType.RAVEN_SENT; + if ("raven_opened".equals(type)) return RavenExpiringMediaType.RAVEN_OPENED; + if ("raven_blocked".equals(type)) return RavenExpiringMediaType.RAVEN_BLOCKED; + if ("raven_sending".equals(type)) return RavenExpiringMediaType.RAVEN_SENDING; + if ("raven_replayed".equals(type)) return RavenExpiringMediaType.RAVEN_REPLAYED; + if ("raven_delivered".equals(type)) return RavenExpiringMediaType.RAVEN_DELIVERED; + if ("raven_suggested".equals(type)) return RavenExpiringMediaType.RAVEN_SUGGESTED; + if ("raven_screenshot".equals(type)) return RavenExpiringMediaType.RAVEN_SCREENSHOT; + if ("raven_cannot_deliver".equals(type)) return RavenExpiringMediaType.RAVEN_CANNOT_DELIVER; + //if ("raven_unknown".equals(type)) [default?] + return RavenExpiringMediaType.RAVEN_UNKNOWN; + } + + public static int convertDpToPx(final float dp) { + if (displayMetrics == null) + displayMetrics = Resources.getSystem().getDisplayMetrics(); + return Math.round((dp * displayMetrics.densityDpi) / 160.0f); + } + + public static void changeTheme() { + int themeCode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; // this is fallback / default + + if (settingsHelper != null) themeCode = settingsHelper.getThemeCode(false); + + if (themeCode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM && Build.VERSION.SDK_INT < 29) + themeCode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; + + AppCompatDelegate.setDefaultNightMode(themeCode); + } + + public static void setTooltipText(final View view, @StringRes final int tooltipTextRes) { + if (view != null && tooltipTextRes != 0 && tooltipTextRes != -1) { + final Context context = view.getContext(); + final String tooltipText = context.getResources().getString(tooltipTextRes); + + if (Build.VERSION.SDK_INT >= 26) view.setTooltipText(tooltipText); + else view.setOnLongClickListener(v -> { + Toast.makeText(context, tooltipText, Toast.LENGTH_SHORT).show(); + return true; + }); + } + } + + @NonNull + public static String millisToString(final long timeMs) { + final long totalSeconds = timeMs / 1000; + + final long seconds = totalSeconds % 60; + final long minutes = totalSeconds / 60 % 60; + final long hours = totalSeconds / 3600; + + final String strSec = Long.toString(seconds); + final String strMin = Long.toString(minutes); + + final String strRetSec = strSec.length() > 1 ? strSec : "0" + seconds; + final String strRetMin = strMin.length() > 1 ? strMin : "0" + minutes; + + final String retMinSec = strRetMin + ':' + strRetSec; + + if (hours > 0) + return Long.toString(hours) + ':' + retMinSec; + return retMinSec; + } + + // extracted from String class + public static int indexOfChar(@NonNull final CharSequence sequence, final int ch, final int startIndex) { + final int max = sequence.length(); + if (startIndex < max) { + if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { + for (int i = startIndex; i < max; i++) if (sequence.charAt(i) == ch) return i; + } else if (Character.isValidCodePoint(ch)) { + final char hi = (char) ((ch >>> 10) + (Character.MIN_HIGH_SURROGATE - (Character.MIN_SUPPLEMENTARY_CODE_POINT >>> 10))); + final char lo = (char) ((ch & 0x3ff) + Character.MIN_LOW_SURROGATE); + for (int i = startIndex; i < max; i++) + if (sequence.charAt(i) == hi && sequence.charAt(i + 1) == lo) return i; + } + } + return -1; + } + + public static boolean hasMentions(final CharSequence text) { + if (isEmpty(text)) return false; + return Utils.indexOfChar(text, '@', 0) != -1 || Utils.indexOfChar(text, '#', 0) != -1; + } + + public static void copyText(final Context context, final CharSequence string) { + final boolean ctxNotNull = context != null; + if (ctxNotNull && clipboardManager == null) + clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + + int toastMessage = R.string.clipboard_error; + if (clipboardManager != null) { + clipboardManager.setPrimaryClip(ClipData.newPlainText(Utils.CHANNEL_NAME, string)); + toastMessage = R.string.clipboard_copied; + } + if (ctxNotNull) Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show(); + } + + @NonNull + public static String readFromConnection(@NonNull final HttpURLConnection conn) throws Exception { + final StringBuilder sb = new StringBuilder(); + try (final BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { + String line; + while ((line = br.readLine()) != null) sb.append(line).append('\n'); + } + return sb.toString(); + } + + public static void batchDownload(@NonNull final Context context, @Nullable final String username, final DownloadMethod method, + final List itemsToDownload) { + if (settingsHelper == null) settingsHelper = new SettingsHelper(context); + + if (itemsToDownload == null || itemsToDownload.size() < 1) return; + + if (ContextCompat.checkSelfPermission(context, Utils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) + batchDownloadImpl(context, username, method, itemsToDownload); + else if (context instanceof Activity) + ActivityCompat.requestPermissions((Activity) context, Utils.PERMS, 8020); + } + + private static void batchDownloadImpl(@NonNull final Context context, @Nullable final String username, + final DownloadMethod method, final List itemsToDownload) { + 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) && !isEmpty(username)) + dir = new File(dir, username); + + if (dir.exists() || dir.mkdirs()) { + final Main main = method != DownloadMethod.DOWNLOAD_FEED && context instanceof Main ? (Main) context : null; + + final int itemsToDownloadSize = itemsToDownload.size(); + + final File finalDir = dir; + for (int i = itemsToDownloadSize - 1; i >= 0; i--) { + final BasePostModel selectedItem = itemsToDownload.get(i); + + if (main == null) { + new DownloadAsync(context, + selectedItem.getDisplayUrl(), + getDownloadSaveFile(finalDir, selectedItem, ""), + null).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + } else { + new PostFetcher(selectedItem.getShortCode(), result -> { + if (result != null) { + final int resultsSize = result.length; + final boolean multiResult = resultsSize > 1; + + for (int j = 0; j < resultsSize; j++) { + final BasePostModel model = result[j]; + final File saveFile = getDownloadSaveFile(finalDir, model, multiResult ? "_slide_" + (j + 1) : ""); + + new DownloadAsync(context, + model.getDisplayUrl(), + saveFile, + file -> { + model.setDownloaded(true); + main.mainHelper.deselectSelection(selectedItem); + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } else { + main.mainHelper.deselectSelection(selectedItem); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } + } else + Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show(); + } + + @NonNull + private static File getDownloadSaveFile(final File finalDir, @NonNull final BasePostModel model, final String sliderPrefix) { + final String displayUrl = model.getDisplayUrl(); + return new File(finalDir, model.getPostId() + '_' + model.getTimestamp() + sliderPrefix + + getExtensionFromModel(displayUrl, model)); + } + + @NonNull + public static String getExtensionFromModel(@NonNull final String url, final Object model) { + final String extension; + final int index = url.indexOf('?'); + + if (index != -1) extension = url.substring(index - 4, index); + else { + final boolean isVideo; + if (model instanceof StoryModel) + isVideo = ((StoryModel) model).getItemType() == MediaItemType.MEDIA_TYPE_VIDEO; + else if (model instanceof BasePostModel) + isVideo = ((BasePostModel) model).getItemType() == MediaItemType.MEDIA_TYPE_VIDEO; + else + isVideo = false; + extension = isVideo || url.contains(".mp4") ? ".mp4" : ".jpg"; + } + + return extension; + } + + public static void checkExistence(final File downloadDir, final File customDir, final String username, final boolean isSlider, + final int sliderIndex, @NonNull final BasePostModel model) { + boolean exists = false; + + try { + final String displayUrl = model.getDisplayUrl(); + final int index = displayUrl.indexOf('?'); + + final String fileName = model.getPostId() + '_' + model.getTimestamp(); + final String extension = displayUrl.substring(index - 4, index); + + final String fileWithoutPrefix = fileName + extension; + exists = new File(downloadDir, fileWithoutPrefix).exists(); + if (!exists) { + if (customDir != null) exists = new File(customDir, fileWithoutPrefix).exists(); + if (!exists && !Utils.isEmpty(username)) { + exists = new File(new File(downloadDir, username), fileWithoutPrefix).exists(); + } + if (!exists && customDir != null) + exists = new File(new File(customDir, username), fileWithoutPrefix).exists(); + } + + if (!exists && isSlider && sliderIndex != -1) { + final String fileWithPrefix = fileName + "_slide_[\\d]+" + extension; + final FilenameFilter filenameFilter = (dir, name) -> Pattern.matches(fileWithPrefix, name); + + File[] files = downloadDir.listFiles(filenameFilter); + if ((files == null || files.length < 1) && customDir != null) + files = customDir.listFiles(filenameFilter); + + if (files != null && files.length >= 1) exists = true; + } + } catch (final Exception e) { + if (logCollector != null) + logCollector.appendException(e, LogCollector.LogFile.UTILS, "checkExistence", + new Pair<>("isSlider", isSlider), + new Pair<>("sliderIndex", sliderIndex), + new Pair<>("model", model)); + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + + model.setDownloaded(exists); + } + + public static boolean hasKey(final String key, final String username, final String name) { + if (!Utils.isEmpty(key)) { + final boolean hasUserName = username != null && username.toLowerCase().contains(key); + if (!hasUserName && name != null) return name.toLowerCase().contains(key); + } + return true; + } + + public static void showImportExportDialog(final Context context) { + final DialogImportExportBinding importExportBinding = DialogImportExportBinding.inflate(LayoutInflater.from(context)); + + final View passwordParent = (View) importExportBinding.cbPassword.getParent(); + final View exportLoginsParent = (View) importExportBinding.cbExportLogins.getParent(); + final View exportFavoritesParent = (View) importExportBinding.cbExportFavorites.getParent(); + final View exportSettingsParent = (View) importExportBinding.cbExportSettings.getParent(); + final View importLoginsParent = (View) importExportBinding.cbImportLogins.getParent(); + final View importFavoritesParent = (View) importExportBinding.cbImportFavorites.getParent(); + final View importSettingsParent = (View) importExportBinding.cbImportSettings.getParent(); + + importExportBinding.cbPassword.setOnCheckedChangeListener((buttonView, isChecked) -> + importExportBinding.etPassword.etPassword.setEnabled(isChecked)); + + final AlertDialog[] dialog = new AlertDialog[1]; + final View.OnClickListener onClickListener = v -> { + if (v == passwordParent) importExportBinding.cbPassword.performClick(); + + else if (v == exportLoginsParent) importExportBinding.cbExportLogins.performClick(); + else if (v == exportFavoritesParent) importExportBinding.cbExportFavorites.performClick(); + + else if (v == importLoginsParent) importExportBinding.cbImportLogins.performClick(); + else if (v == importFavoritesParent) importExportBinding.cbImportFavorites.performClick(); + + else if (v == exportSettingsParent) importExportBinding.cbExportSettings.performClick(); + else if (v == importSettingsParent) importExportBinding.cbImportSettings.performClick(); + + else if (context instanceof AppCompatActivity) { + final FragmentManager fragmentManager = ((AppCompatActivity) context).getSupportFragmentManager(); + final String folderPath = settingsHelper.getString(FOLDER_PATH); + + if (v == importExportBinding.btnSaveTo) { + final Editable text = importExportBinding.etPassword.etPassword.getText(); + final boolean passwordChecked = importExportBinding.cbPassword.isChecked(); + if (passwordChecked && isEmpty(text)) + Toast.makeText(context, R.string.dialog_export_err_password_empty, Toast.LENGTH_SHORT).show(); + else { + new DirectoryChooser().setInitialDirectory(folderPath).setInteractionListener(path -> { + final File file = new File(path, "InstaGrabber_Settings_" + System.currentTimeMillis() + ".zaai"); + final String password = passwordChecked ? text.toString() : null; + int flags = 0; + if (importExportBinding.cbExportFavorites.isChecked()) flags |= ExportImportUtils.FLAG_FAVORITES; + if (importExportBinding.cbExportSettings.isChecked()) flags |= ExportImportUtils.FLAG_SETTINGS; + if (importExportBinding.cbExportLogins.isChecked()) flags |= ExportImportUtils.FLAG_COOKIES; + + ExportImportUtils.Export(password, flags, file, result -> { + Toast.makeText(context, result ? R.string.dialog_export_success : R.string.dialog_export_failed, Toast.LENGTH_SHORT).show(); + if (dialog[0] != null && dialog[0].isShowing()) dialog[0].dismiss(); + }); + + }).show(fragmentManager, null); + } + + } else if (v == importExportBinding.btnImport) { + new DirectoryChooser().setInitialDirectory(folderPath).setShowZaAiConfigFiles(true).setInteractionListener(path -> { + int flags = 0; + if (importExportBinding.cbImportFavorites.isChecked()) flags |= ExportImportUtils.FLAG_FAVORITES; + if (importExportBinding.cbImportSettings.isChecked()) flags |= ExportImportUtils.FLAG_SETTINGS; + if (importExportBinding.cbImportLogins.isChecked()) flags |= ExportImportUtils.FLAG_COOKIES; + + ExportImportUtils.Import(context, flags, new File(path), result -> { + ((AppCompatActivity) context).recreate(); + Toast.makeText(context, result ? R.string.dialog_import_success : R.string.dialog_import_failed, Toast.LENGTH_SHORT).show(); + if (dialog[0] != null && dialog[0].isShowing()) dialog[0].dismiss(); + }); + + }).show(fragmentManager, null); + } + } + }; + + passwordParent.setOnClickListener(onClickListener); + exportLoginsParent.setOnClickListener(onClickListener); + exportSettingsParent.setOnClickListener(onClickListener); + exportFavoritesParent.setOnClickListener(onClickListener); + importLoginsParent.setOnClickListener(onClickListener); + importSettingsParent.setOnClickListener(onClickListener); + importFavoritesParent.setOnClickListener(onClickListener); + importExportBinding.btnSaveTo.setOnClickListener(onClickListener); + importExportBinding.btnImport.setOnClickListener(onClickListener); + + dialog[0] = new AlertDialog.Builder(context).setView(importExportBinding.getRoot()).show(); + } + + // taken from Arrays.toString() + @NonNull + public static String highlightIdsMerger(final String... strings) { + if (strings != null) { + int iMax = strings.length - 1; + if (iMax != -1) { + final StringBuilder builder = new StringBuilder(); + builder.append('['); + for (int i = 0; ; i++) { + builder.append('"').append(strings[i]).append('"'); + if (i == iMax) return builder.append(']').toString(); + builder.append(','); + } + } + + } + return "[]"; + } + + public static void putHighlightModels(final HttpURLConnection conn, final Object[] model) throws Exception { + final boolean isHighlightModel = model instanceof HighlightModel[]; + final boolean isFeedStoryModel = model instanceof FeedStoryModel[]; + + if (isHighlightModel || isFeedStoryModel) { + final JSONArray highlightsMediaReel = new JSONObject(Utils.readFromConnection(conn)).getJSONObject("data").getJSONArray("reels_media"); + final int mediaLength = highlightsMediaReel.length(); + + for (int i = 0; i < mediaLength; ++i) { + final JSONArray items = highlightsMediaReel.getJSONObject(i).getJSONArray("items"); + final int itemsLen = items.length(); + + final StoryModel[] storyModels = new StoryModel[itemsLen]; + for (int j = 0; j < itemsLen; ++j) { + final JSONObject data = items.getJSONObject(j); + + final boolean isVideo = data.getBoolean("is_video"); + + boolean hasTappableObjecs = data.has("tappable_objects"); + final JSONArray tappableObjects; + final int tappableLength; + if (hasTappableObjecs) { + tappableObjects = data.getJSONArray("tappable_objects"); + tappableLength = tappableObjects.length(); + hasTappableObjecs = tappableLength > 0; + } else { + tappableLength = 0; + tappableObjects = null; + } + + storyModels[j] = new StoryModel(data.getString(Constants.EXTRAS_ID), data.getString("display_url"), + isVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE, + data.getLong("taken_at_timestamp")); + + if (isVideo && data.has("video_resources")) + storyModels[j].setVideoUrl(Utils.getHighQualityPost(data.getJSONArray("video_resources"), true)); + + if (hasTappableObjecs) { + for (int k = 0; k < tappableLength; ++k) { + JSONObject jsonObject = tappableObjects.getJSONObject(k); + if (jsonObject.getString("__typename").equals("GraphTappableFeedMedia") && jsonObject.has("media")) { + jsonObject = jsonObject.getJSONObject("media"); + storyModels[j].setTappableShortCode(jsonObject.getString(Constants.EXTRAS_SHORTCODE)); + break; + } + } + } + } + + if (isHighlightModel) + ((HighlightModel[]) model)[i].setStoryModels(storyModels); + else + ((FeedStoryModel[]) model)[i].setStoryModels(storyModels); + } + } + } + + public static CharSequence getSpannableUrl(final String url) { + if (Utils.isEmpty(url)) return url; + final int httpIndex = url.indexOf("http:"); + final int httpsIndex = url.indexOf("https:"); + if (httpIndex == -1 && httpsIndex == -1) return url; + + final int length = url.length(); + + final int startIndex = httpIndex != -1 ? httpIndex : httpsIndex; + final int spaceIndex = url.indexOf(' ', startIndex + 1); + + final int endIndex = (spaceIndex != -1 ? spaceIndex : length); + + final String extractUrl = url.substring(startIndex, Math.min(length, endIndex) - 1); + + final SpannableString spannableString = new SpannableString(url); + spannableString.setSpan(new URLSpan(extractUrl), startIndex, endIndex, 0); + + return spannableString; + } + + public static boolean isEmpty(final CharSequence charSequence) { + if (charSequence == null || charSequence.length() < 1) return true; + if (charSequence instanceof String) { + String str = (String) charSequence; + if ("".equals(str) || "null".equals(str) || str.isEmpty()) return true; + str = str.trim(); + return "".equals(str) || "null".equals(str) || str.isEmpty(); + } + return "null".contentEquals(charSequence) || "".contentEquals(charSequence) || charSequence.length() < 1; + } + + public static boolean isImage(final Uri itemUri, final ContentResolver contentResolver) { + String mimeType; + if (itemUri == null) return false; + final String scheme = itemUri.getScheme(); + if (isEmpty(scheme)) + mimeType = mimeTypeMap.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(itemUri.toString()).toLowerCase()); + else mimeType = scheme.equals(ContentResolver.SCHEME_CONTENT) ? contentResolver.getType(itemUri) + : mimeTypeMap.getMimeTypeFromExtension + (MimeTypeMap.getFileExtensionFromUrl(itemUri.toString()).toLowerCase()); + + if (isEmpty(mimeType)) return true; + mimeType = mimeType.toLowerCase(); + return mimeType.startsWith("image"); + } + + @Nullable + public static String getCookie(@Nullable final String webViewUrl) { + int lastLongestCookieLength = 0; + String mainCookie = null; + + String cookie; + if (!Utils.isEmpty(webViewUrl)) { + cookie = Utils.COOKIE_MANAGER.getCookie(webViewUrl); + if (cookie != null) { + final int cookieLen = cookie.length(); + if (cookieLen > lastLongestCookieLength) { + mainCookie = cookie; + lastLongestCookieLength = cookieLen; + } + } + } + cookie = Utils.COOKIE_MANAGER.getCookie("https://instagram.com"); + if (cookie != null) { + final int cookieLen = cookie.length(); + if (cookieLen > lastLongestCookieLength) { + mainCookie = cookie; + lastLongestCookieLength = cookieLen; + } + } + cookie = Utils.COOKIE_MANAGER.getCookie("https://instagram.com/"); + if (cookie != null) { + final int cookieLen = cookie.length(); + if (cookieLen > lastLongestCookieLength) { + mainCookie = cookie; + lastLongestCookieLength = cookieLen; + } + } + cookie = Utils.COOKIE_MANAGER.getCookie("http://instagram.com"); + if (cookie != null) { + final int cookieLen = cookie.length(); + if (cookieLen > lastLongestCookieLength) { + mainCookie = cookie; + lastLongestCookieLength = cookieLen; + } + } + cookie = Utils.COOKIE_MANAGER.getCookie("http://instagram.com/"); + if (cookie != null) { + final int cookieLen = cookie.length(); + if (cookieLen > lastLongestCookieLength) { + mainCookie = cookie; + lastLongestCookieLength = cookieLen; + } + } + cookie = Utils.COOKIE_MANAGER.getCookie("https://www.instagram.com"); + if (cookie != null) { + final int cookieLen = cookie.length(); + if (cookieLen > lastLongestCookieLength) { + mainCookie = cookie; + lastLongestCookieLength = cookieLen; + } + } + cookie = Utils.COOKIE_MANAGER.getCookie("https://www.instagram.com/"); + if (cookie != null) { + final int cookieLen = cookie.length(); + if (cookieLen > lastLongestCookieLength) { + mainCookie = cookie; + lastLongestCookieLength = cookieLen; + } + } + cookie = Utils.COOKIE_MANAGER.getCookie("http://www.instagram.com"); + if (cookie != null) { + final int cookieLen = cookie.length(); + if (cookieLen > lastLongestCookieLength) { + mainCookie = cookie; + lastLongestCookieLength = cookieLen; + } + } + cookie = Utils.COOKIE_MANAGER.getCookie("http://www.instagram.com/"); + if (cookie != null && cookie.length() > lastLongestCookieLength) mainCookie = cookie; + + return mainCookie; + } + + public static void errorFinish(@NonNull final Activity activity) { + Toast.makeText(activity, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + activity.finish(); + } + + public static boolean isInstaInstalled(@NonNull final Context context) { + final PackageManager packageManager = context.getPackageManager(); + try { + packageManager.getPackageInfo("com.instagram.android", 0); + return true; + } catch (final Exception e) { + try { + return packageManager.getApplicationInfo("com.instagram.android", 0).enabled; + } catch (final Exception e1) { + return false; + } + } + } + + @Nullable + public static String getInstalledTelegramPackage(@NonNull final Context context) { + final String[] packages = { + "org.telegram.messenger", + "org.thunderdog.challegram", + "ir.ilmili.telegraph", + "org.telegram.BifToGram", + "org.vidogram.messenger", + "com.xplus.messenger", + "com.ellipi.messenger", + "org.telegram.plus", + "com.iMe.android", + "org.viento.colibri", + "org.viento.colibrix", + "ml.parsgram", + "com.ringtoon.app.tl", + }; + + final PackageManager packageManager = context.getPackageManager(); + for (final String pkg : packages) { + try { + final PackageInfo packageInfo = packageManager.getPackageInfo(pkg, 0); + if (packageInfo.applicationInfo.enabled) return pkg; + } catch (final Exception e) { + try { + if (packageManager.getApplicationInfo(pkg, 0).enabled) return pkg; + } catch (final Exception e1) { + // meh + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/awaisomereport/CrashReporter.java b/app/src/main/java/awaisomereport/CrashReporter.java new file mode 100755 index 00000000..1884069b --- /dev/null +++ b/app/src/main/java/awaisomereport/CrashReporter.java @@ -0,0 +1,197 @@ +package awaisomereport; + +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Date; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import awais.instagrabber.BuildConfig; +import awais.instagrabber.utils.Utils; + +public final class CrashReporter implements Thread.UncaughtExceptionHandler { + private static CrashReporter reporterInstance; + private final Application application; + private final String email; + private final File crashLogsZip; + private boolean startAttempted = false; + + public static CrashReporter get(final Application application) { + if (reporterInstance == null) reporterInstance = new CrashReporter(application); + return reporterInstance; + } + + private CrashReporter(@NonNull final Application application) { + this.application = application; + // set your email(s) here to receive crash reports + // this.email = ""; + this.email = "chapter50000@hotmail.com"; + this.crashLogsZip = new File(application.getExternalCacheDir(), "crash_logs.zip"); + } + + public void start() { + if (!startAttempted) { + Thread.setDefaultUncaughtExceptionHandler(this); + startAttempted = true; + } + } + + @Override + public void uncaughtException(@NonNull final Thread t, @NonNull final Throwable exception) { + final StringBuilder reportBuilder = new StringBuilder(); + reportBuilder.append("Error report collected on: ").append(new Date().toString()); + reportBuilder.append("\r\n\r\nInformation:\r\n=============="); + + reportBuilder + .append("\r\nVERSION : ").append(BuildConfig.VERSION_NAME) + .append("\r\nVERSION_CODE : ").append(BuildConfig.VERSION_CODE) + .append("\r\nPHONE-MODEL : ").append(Build.MODEL) + .append("\r\nANDROID_VERS : ").append(Build.VERSION.RELEASE) + .append("\r\nANDROID_REL : ").append(Build.VERSION.SDK_INT) + .append("\r\nBRAND : ").append(Build.BRAND) + .append("\r\nMANUFACTURER : ").append(Build.MANUFACTURER) + .append("\r\nBOARD : ").append(Build.BOARD) + .append("\r\nDEVICE : ").append(Build.DEVICE) + .append("\r\nPRODUCT : ").append(Build.PRODUCT) + .append("\r\nHOST : ").append(Build.HOST) + .append("\r\nTAGS : ").append(Build.TAGS); + + reportBuilder.append("\r\n\r\nStack:\r\n==============\r\n"); + final Writer result = new StringWriter(); + try (final PrintWriter printWriter = new PrintWriter(result)) { + exception.printStackTrace(printWriter); + reportBuilder.append(result.toString()); + + reportBuilder.append("\r\nCause:\r\n=============="); + + // for AsyncTask crashes + Throwable cause = exception.getCause(); + while (cause != null) { + cause.printStackTrace(printWriter); + reportBuilder.append(result.toString()); + cause = cause.getCause(); + } + } + reportBuilder.append("\r\n\r\n**** End of current Report ***"); + + final String errorContent = reportBuilder.toString(); + try (final FileOutputStream trace = application.openFileOutput("stack-" + System.currentTimeMillis() + ".stacktrace", Context.MODE_PRIVATE)) { + trace.write(errorContent.getBytes()); + } catch (final Exception ex) { + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", ex); + } + + application.startActivity(new Intent(application, ErrorReporterActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + + zipLogs(); + + Process.killProcess(Process.myPid()); + System.exit(10); + } + + public synchronized CrashReporter zipLogs() { + final File logDir = Utils.logCollector != null ? Utils.logCollector.getLogDir() : + new File(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? application.getDataDir() : application.getFilesDir(), "ur_mom_gay_logs"); + + try (final FileOutputStream fos = new FileOutputStream(crashLogsZip); + final ZipOutputStream zos = new ZipOutputStream(fos)) { + + final File[] files = logDir.listFiles(); + + if (files != null) { + zos.setLevel(5); + byte[] buffer; + for (final File file : files) { + if (file != null && file.length() > 0) { + buffer = new byte[1024]; + try (final FileInputStream fis = new FileInputStream(file)) { + zos.putNextEntry(new ZipEntry(file.getName())); + int length; + while ((length = fis.read(buffer)) > 0) zos.write(buffer, 0, length); + zos.closeEntry(); + } + } + } + } + + } catch (final Exception e) { + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + + return this; + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public void startCrashEmailIntent(final Context context, final boolean sendZipsOnly) { + try { + final String filePath = context.getFilesDir().getAbsolutePath(); + + String[] errorFileList; + + if (sendZipsOnly) errorFileList = null; + else { + try { + final File dir = new File(filePath); + if (dir.exists() && !dir.isDirectory()) dir.delete(); + dir.mkdir(); + errorFileList = dir.list((d, name) -> name.endsWith(".stacktrace")); + } catch (final Exception e) { + errorFileList = null; + } + } + + if ((errorFileList != null && errorFileList.length > 0) || sendZipsOnly) { + final StringBuilder errorStringBuilder; + + if (sendZipsOnly) errorStringBuilder = new StringBuilder("So... what happened?\n\n"); + else { + errorStringBuilder = new StringBuilder("\r\n\r\n"); + final int maxSendMail = 5; + + int curIndex = 0; + for (final String curString : errorFileList) { + final File file = new File(filePath + '/' + curString); + + if (curIndex++ <= maxSendMail) { + errorStringBuilder.append("New Trace collected:\r\n=====================\r\n"); + try (final BufferedReader input = new BufferedReader(new FileReader(file))) { + String line; + while ((line = input.readLine()) != null) + errorStringBuilder.append(line).append("\r\n"); + } + } + + file.delete(); + } + + errorStringBuilder.append("\r\n\r\n"); + } + + context.startActivity(Intent.createChooser(new Intent(Intent.ACTION_SEND).setType("message/rfc822") + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + .putExtra(Intent.EXTRA_EMAIL, new String[]{email}) + .putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(application, BuildConfig.APPLICATION_ID + ".provider", crashLogsZip)) + .putExtra(Intent.EXTRA_SUBJECT, "InstaGrabber Crash Report") + .putExtra(Intent.EXTRA_TEXT, errorStringBuilder.toString()), "Select an email app to send crash logs")); + } + } catch (final Exception e) { + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awaisomereport/ErrorReporterActivity.java b/app/src/main/java/awaisomereport/ErrorReporterActivity.java new file mode 100755 index 00000000..5bd4669c --- /dev/null +++ b/app/src/main/java/awaisomereport/ErrorReporterActivity.java @@ -0,0 +1,101 @@ +package awaisomereport; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ImageSpan; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; + +import awais.instagrabber.R; + +public final class ErrorReporterActivity extends Activity implements View.OnClickListener { + private View btnReport; + + @Override + protected void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_crash_error); + + setFinishOnTouchOutside(false); + getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + final SpannableString crashTitle = new SpannableString(" " + getString(R.string.crash_title)); + crashTitle.setSpan(new CenteredImageSpan(this, android.R.drawable.stat_notify_error), + 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + setTitle(crashTitle); + + btnReport = findViewById(R.id.btnReport); + btnReport.setOnClickListener(this); + findViewById(R.id.btnCancel).setOnClickListener(this); + } + + @Override + public void onClick(@NonNull final View v) { + if (v == btnReport) + CrashReporter.get(getApplication()).startCrashEmailIntent(this, false); + finish(); + System.exit(10); + } + + public static class CenteredImageSpan extends ImageSpan { + private WeakReference drawable; + + public CenteredImageSpan(final Context context, final int drawableRes) { + super(context, drawableRes); + } + + @Override + public int getSize(@NonNull final Paint paint, final CharSequence text, final int start, final int end, @Nullable final Paint.FontMetricsInt fm) { + final Drawable drawable = getCachedDrawable(); + final Rect rect = drawable.getBounds(); + + if (fm != null) { + final Paint.FontMetricsInt pfm = paint.getFontMetricsInt(); + fm.ascent = pfm.ascent; + fm.descent = pfm.descent; + fm.top = pfm.top; + fm.bottom = pfm.bottom; + } + + return rect.right; + } + + @Override + public void draw(@NonNull final Canvas canvas, final CharSequence text, final int start, final int end, final float x, final int top, + final int y, final int bottom, @NonNull final Paint paint) { + final Drawable drawable = getCachedDrawable(); + canvas.save(); + + final int drawableHeight = drawable.getIntrinsicHeight(); + final Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt(); + int transY = bottom - drawable.getBounds().bottom + (drawableHeight - fontMetricsInt.descent + fontMetricsInt.ascent) / 2; + + canvas.translate(x, transY); + drawable.draw(canvas); + canvas.restore(); + } + + private Drawable getCachedDrawable() { + Drawable d = null; + if (drawable != null) d = drawable.get(); + if (d == null) { + d = getDrawable(); + drawable = new WeakReference<>(d); + } + return d; + } + } +} + diff --git a/app/src/main/java/awaisomereport/LogCollector.java b/app/src/main/java/awaisomereport/LogCollector.java new file mode 100755 index 00000000..43ca9596 --- /dev/null +++ b/app/src/main/java/awaisomereport/LogCollector.java @@ -0,0 +1,138 @@ +package awaisomereport; + +import android.app.Application; +import android.os.Build; +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +import awais.instagrabber.BuildConfig; + +public final class LogCollector { + private final File logDir; + + public LogCollector(@NonNull final Application app) { + logDir = new File(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? app.getDataDir() : app.getFilesDir(), + "ur_mom_gay_logs"); + + if (logDir.exists()) deleteRecursive(logDir); + + if (logDir.mkdirs()) { + // create log files to zip later + for (final LogFile logFile : LogFile.values()) { + try { + //noinspection ResultOfMethodCallIgnored + new File(logDir, logFile.fileName).createNewFile(); + } catch (final IOException e) { + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } + } + } + + public File getLogDir() { + return logDir; + } + + @SafeVarargs + public final void appendException(@NonNull final Exception exception, @NonNull final LogFile logFile, @NonNull final String method, + @Nullable final Pair... vars) { + final File excepionFile = new File(logDir, logFile.fileName); + + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append('\n').append('\n').append("----------------- ").append(method).append(" ------------------") + .append('\n'); + + if (vars != null && vars.length > 0) { + stringBuilder.append("Variables: ").append('\n'); + for (Pair var : vars) + stringBuilder.append('\t').append(var.first).append(" : ") + .append('\u201C').append(var.second).append('\u201D') + .append(" (type: ").append(var.second == null ? "null" : var.second.getClass().getSimpleName()).append(')') + .append('\n'); + stringBuilder.append("----------------------------------").append('\n'); + } + + final Writer stringWriter = new StringWriter(); + try (final PrintWriter printWriter = new PrintWriter(stringWriter)) { + exception.printStackTrace(printWriter); + stringBuilder.append(stringWriter.toString()); + + // for AsyncTask crashes + Throwable cause = exception.getCause(); + while (cause != null) { + cause.printStackTrace(printWriter); + stringBuilder.append(stringWriter.toString()); + cause = cause.getCause(); + } + } + + try (final BufferedReader br = new BufferedReader(new FileReader(excepionFile))) { + String line; + while ((line = br.readLine()) != null) stringBuilder.append(line).append('\n'); + } catch (final Exception e) { + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + + stringBuilder.append('\n'); + + try (final BufferedWriter bw = new BufferedWriter(new FileWriter(excepionFile))) { + bw.write(stringBuilder.toString()); + } catch (final Exception e) { + if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + } + } + + public enum LogFile { + UTILS("utils.txt"), + MAIN_HELPER("main-helper.txt"), + //////////////////////// + ACTIVITY_STORY_VIEWER("act-story-viewer.txt"), + //////////////////////// + ASYNC_DOWNLOADER("async-download.txt"), + ASYNC_MAIN_POSTS_FETCHER("async-main-posts-fetcher.txt"), + ASYNC_POST_FETCHER("async-single-post-fetcher.txt"), + ASYNC_FEED_FETCHER("async-feed-fetcher.txt"), + ASYNC_PROFILE_FETCHER("async-profile-fetcher.txt"), + ASYNC_PROFILE_PICTURE_FETCHER("async-pfp-fetcher.txt"), + ASYNC_STORY_STATUS_FETCHER("async-story-status-fetcher.txt"), + ASYNC_DISCOVER_FETCHER("async-discover-fetcher.txt"), + ASYNC_COMMENTS_FETCHER("async-comments-fetcher.txt"), + ASYNC_FOLLOW_FETCHER("async-follow-fetcher.txt"), + ASYNC_FEED_STORY_FETCHER("async-feed-story-fetcher.txt"), + //////////////////////// + ASYNC_DMS("async-dms-inbox-fetcher.txt"), + ASYNC_DMS_THREAD("async-dms-thread-fetcher.txt"), + //////////////////////// + DATA_BOX_FAVORITES("data-box-favs.txt"), + UTILS_EXPORT("utils-export.txt"), + UTILS_IMPORT("utils-import.txt"), + ; + private final String fileName; + + LogFile(final String fileName) { + this.fileName = fileName; + } + } + + private static void deleteRecursive(@NonNull final File fileOrDirectory) { + final File[] files; + if (fileOrDirectory.isDirectory() && (files = fileOrDirectory.listFiles()) != null) + for (final File child : files) deleteRecursive(child); + //noinspection ResultOfMethodCallIgnored + fileOrDirectory.delete(); + } +} \ No newline at end of file diff --git a/app/src/main/java/thoughtbot/expandableadapter/ExpandableGroup.java b/app/src/main/java/thoughtbot/expandableadapter/ExpandableGroup.java new file mode 100755 index 00000000..0e499ba8 --- /dev/null +++ b/app/src/main/java/thoughtbot/expandableadapter/ExpandableGroup.java @@ -0,0 +1,40 @@ +package thoughtbot.expandableadapter; + +import java.util.ArrayList; +import java.util.List; + +import awais.instagrabber.models.FollowModel; + +public class ExpandableGroup { + private final String title; + private final List items; + + public ExpandableGroup(final String title, final List items) { + this.title = title; + this.items = items; + } + + public String getTitle() { + return title; + } + + public List getItems(final boolean filtered) { + if (!filtered) return items; + final ArrayList followModels = new ArrayList<>(); + for (final FollowModel followModel : items) if (followModel.isShown()) followModels.add(followModel); + return followModels; + } + + public int getItemCount(final boolean filtered) { + if (items != null) { + final int size = items.size(); + if (filtered) { + int finalSize = 0; + for (int i = 0; i < size; ++i) if (items.get(i).isShown()) ++finalSize; + return finalSize; + } + return size; + } + return 0; + } +} \ No newline at end of file diff --git a/app/src/main/java/thoughtbot/expandableadapter/ExpandableList.java b/app/src/main/java/thoughtbot/expandableadapter/ExpandableList.java new file mode 100755 index 00000000..408e46cf --- /dev/null +++ b/app/src/main/java/thoughtbot/expandableadapter/ExpandableList.java @@ -0,0 +1,51 @@ +package thoughtbot.expandableadapter; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; + +public final class ExpandableList { + private final int groupsSize; + public final ArrayList groups; + public final boolean[] expandedGroupIndexes; + + public ExpandableList(@NonNull final ArrayList groups) { + this.groups = groups; + this.groupsSize = groups.size(); + this.expandedGroupIndexes = new boolean[groupsSize]; + } + + public int getVisibleItemCount() { + int count = 0; + for (int i = 0; i < groupsSize; i++) count = count + numberOfVisibleItemsInGroup(i); + return count; + } + + @NonNull + public ExpandableListPosition getUnflattenedPosition(final int flPos) { + int adapted = flPos; + for (int i = 0; i < groupsSize; i++) { + final int groupItemCount = numberOfVisibleItemsInGroup(i); + if (adapted == 0) + return ExpandableListPosition.obtain(ExpandableListPosition.GROUP, i, -1, flPos); + else if (adapted < groupItemCount) + return ExpandableListPosition.obtain(ExpandableListPosition.CHILD, i, adapted - 1, flPos); + adapted = adapted - groupItemCount; + } + throw new RuntimeException("Unknown state"); + } + + private int numberOfVisibleItemsInGroup(final int group) { + return expandedGroupIndexes[group] ? groups.get(group).getItemCount(true) + 1 : 1; + } + + public int getFlattenedGroupIndex(@NonNull final ExpandableListPosition listPosition) { + int runningTotal = 0; + for (int i = 0; i < listPosition.groupPos; i++) runningTotal = runningTotal + numberOfVisibleItemsInGroup(i); + return runningTotal; + } + + public ExpandableGroup getExpandableGroup(@NonNull ExpandableListPosition listPosition) { + return groups.get(listPosition.groupPos); + } +} \ No newline at end of file diff --git a/app/src/main/java/thoughtbot/expandableadapter/ExpandableListPosition.java b/app/src/main/java/thoughtbot/expandableadapter/ExpandableListPosition.java new file mode 100755 index 00000000..3a2e33ef --- /dev/null +++ b/app/src/main/java/thoughtbot/expandableadapter/ExpandableListPosition.java @@ -0,0 +1,41 @@ +package thoughtbot.expandableadapter; + +import androidx.annotation.NonNull; + +public class ExpandableListPosition { + private static final ExpandableListPosition LIST_POSITION = new ExpandableListPosition(); + public final static int CHILD = 1; + public final static int GROUP = 2; + private int flatListPos; + public int groupPos; + public int childPos; + public int type; + + @NonNull + public static ExpandableListPosition obtain(final int type, final int groupPos, final int childPos, final int flatListPos) { + LIST_POSITION.type = type; + LIST_POSITION.groupPos = groupPos; + LIST_POSITION.childPos = childPos; + LIST_POSITION.flatListPos = flatListPos; + return LIST_POSITION; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + //if (o != null && getClass() == o.getClass()) { + if (o instanceof ExpandableListPosition) { + final ExpandableListPosition that = (ExpandableListPosition) o; + if (groupPos != that.groupPos) return false; + if (childPos != that.childPos) return false; + if (flatListPos != that.flatListPos) return false; + return type == that.type; + } + return false; + } + + @Override + public int hashCode() { + return 31 * (31 * (31 * groupPos + childPos) + flatListPos) + type; + } +} \ No newline at end of file diff --git a/app/src/main/java/thoughtbot/expandableadapter/GroupViewHolder.java b/app/src/main/java/thoughtbot/expandableadapter/GroupViewHolder.java new file mode 100755 index 00000000..075174b6 --- /dev/null +++ b/app/src/main/java/thoughtbot/expandableadapter/GroupViewHolder.java @@ -0,0 +1,39 @@ +package thoughtbot.expandableadapter; + +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.interfaces.OnGroupClickListener; + +public class GroupViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + private final OnGroupClickListener listener; + private final TextView title; + private final ImageView arrow; + + public GroupViewHolder(@NonNull final View itemView, final OnGroupClickListener listener) { + super(itemView); + this.listener = listener; + this.title = itemView.findViewById(android.R.id.text1); + this.arrow = itemView.findViewById(R.id.collapsingArrow); + this.title.setBackgroundColor(0x80_1565C0); + itemView.setOnClickListener(this); + } + + public void setTitle(@NonNull final String title) { + this.title.setText(title); + } + + @Override + public void onClick(final View v) { + if (listener != null) listener.toggleGroup(getLayoutPosition()); + } + + public void toggle(final boolean expand) { + arrow.setImageResource(expand ? R.drawable.collapse : R.drawable.expand); + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_download.png~ b/app/src/main/res/drawable-hdpi/ic_download.png~ new file mode 100755 index 00000000..a81ad200 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_download.png~ differ diff --git a/app/src/main/res/drawable-hdpi/nav_up.png b/app/src/main/res/drawable-hdpi/nav_up.png new file mode 100755 index 00000000..99622e7c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/nav_up.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_download.png~ b/app/src/main/res/drawable-mdpi/ic_download.png~ new file mode 100755 index 00000000..9e8459a5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_download.png~ differ diff --git a/app/src/main/res/drawable-mdpi/nav_up.png b/app/src/main/res/drawable-mdpi/nav_up.png new file mode 100755 index 00000000..f2b35ac2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/nav_up.png differ diff --git a/app/src/main/res/drawable-night-hdpi/nav_up.png b/app/src/main/res/drawable-night-hdpi/nav_up.png new file mode 100755 index 00000000..d7b27da8 Binary files /dev/null and b/app/src/main/res/drawable-night-hdpi/nav_up.png differ diff --git a/app/src/main/res/drawable-night-mdpi/nav_up.png b/app/src/main/res/drawable-night-mdpi/nav_up.png new file mode 100755 index 00000000..edd9b1dd Binary files /dev/null and b/app/src/main/res/drawable-night-mdpi/nav_up.png differ diff --git a/app/src/main/res/drawable-night-xhdpi/nav_up.png b/app/src/main/res/drawable-night-xhdpi/nav_up.png new file mode 100755 index 00000000..8ac0552c Binary files /dev/null and b/app/src/main/res/drawable-night-xhdpi/nav_up.png differ diff --git a/app/src/main/res/drawable-night-xxhdpi/nav_up.png b/app/src/main/res/drawable-night-xxhdpi/nav_up.png new file mode 100755 index 00000000..5e61c3d1 Binary files /dev/null and b/app/src/main/res/drawable-night-xxhdpi/nav_up.png differ diff --git a/app/src/main/res/drawable-night/collapse.png b/app/src/main/res/drawable-night/collapse.png new file mode 100755 index 00000000..933450eb Binary files /dev/null and b/app/src/main/res/drawable-night/collapse.png differ diff --git a/app/src/main/res/drawable-night/comments.png b/app/src/main/res/drawable-night/comments.png new file mode 100755 index 00000000..5693f097 Binary files /dev/null and b/app/src/main/res/drawable-night/comments.png differ diff --git a/app/src/main/res/drawable-night/expand.png b/app/src/main/res/drawable-night/expand.png new file mode 100755 index 00000000..4e0a2e62 Binary files /dev/null and b/app/src/main/res/drawable-night/expand.png differ diff --git a/app/src/main/res/drawable-night/expired.png b/app/src/main/res/drawable-night/expired.png new file mode 100755 index 00000000..31797431 Binary files /dev/null and b/app/src/main/res/drawable-night/expired.png differ diff --git a/app/src/main/res/drawable-night/mute.png b/app/src/main/res/drawable-night/mute.png new file mode 100755 index 00000000..734108c9 Binary files /dev/null and b/app/src/main/res/drawable-night/mute.png differ diff --git a/app/src/main/res/drawable-night/video_views.png b/app/src/main/res/drawable-night/video_views.png new file mode 100755 index 00000000..8922fa8a Binary files /dev/null and b/app/src/main/res/drawable-night/video_views.png differ diff --git a/app/src/main/res/drawable-night/vol.png b/app/src/main/res/drawable-night/vol.png new file mode 100755 index 00000000..abd10560 Binary files /dev/null and b/app/src/main/res/drawable-night/vol.png differ diff --git a/app/src/main/res/drawable-xhdpi/nav_up.png b/app/src/main/res/drawable-xhdpi/nav_up.png new file mode 100755 index 00000000..fca5022c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/nav_up.png differ diff --git a/app/src/main/res/drawable-xxhdpi/nav_up.png b/app/src/main/res/drawable-xxhdpi/nav_up.png new file mode 100755 index 00000000..f5299450 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/nav_up.png differ diff --git a/app/src/main/res/drawable/check.png b/app/src/main/res/drawable/check.png new file mode 100755 index 00000000..8874eb00 Binary files /dev/null and b/app/src/main/res/drawable/check.png differ diff --git a/app/src/main/res/drawable/collapse.png b/app/src/main/res/drawable/collapse.png new file mode 100755 index 00000000..711da440 Binary files /dev/null and b/app/src/main/res/drawable/collapse.png differ diff --git a/app/src/main/res/drawable/comments.png b/app/src/main/res/drawable/comments.png new file mode 100755 index 00000000..ab815ec5 Binary files /dev/null and b/app/src/main/res/drawable/comments.png differ diff --git a/app/src/main/res/drawable/downloaded.png b/app/src/main/res/drawable/downloaded.png new file mode 100755 index 00000000..5a08664a Binary files /dev/null and b/app/src/main/res/drawable/downloaded.png differ diff --git a/app/src/main/res/drawable/expand.png b/app/src/main/res/drawable/expand.png new file mode 100755 index 00000000..97f0359f Binary files /dev/null and b/app/src/main/res/drawable/expand.png differ diff --git a/app/src/main/res/drawable/expired.png b/app/src/main/res/drawable/expired.png new file mode 100755 index 00000000..7fe48726 Binary files /dev/null and b/app/src/main/res/drawable/expired.png differ diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100755 index 00000000..0553ae30 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_discover.xml b/app/src/main/res/drawable/ic_discover.xml new file mode 100755 index 00000000..7a1ab400 --- /dev/null +++ b/app/src/main/res/drawable/ic_discover.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100755 index 00000000..3fb23459 --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_feed.xml b/app/src/main/res/drawable/ic_feed.xml new file mode 100755 index 00000000..c163a23d --- /dev/null +++ b/app/src/main/res/drawable/ic_feed.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_fullscreen.xml b/app/src/main/res/drawable/ic_fullscreen.xml new file mode 100755 index 00000000..fefb27a3 --- /dev/null +++ b/app/src/main/res/drawable/ic_fullscreen.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_fullscreen_exit.xml b/app/src/main/res/drawable/ic_fullscreen_exit.xml new file mode 100755 index 00000000..ad756976 --- /dev/null +++ b/app/src/main/res/drawable/ic_fullscreen_exit.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_import_export.xml b/app/src/main/res/drawable/ic_import_export.xml new file mode 100755 index 00000000..2249979f --- /dev/null +++ b/app/src/main/res/drawable/ic_import_export.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_profile.xml b/app/src/main/res/drawable/ic_profile.xml new file mode 100755 index 00000000..568656a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_profile.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/lock.xml b/app/src/main/res/drawable/lock.xml new file mode 100755 index 00000000..4bb23a77 --- /dev/null +++ b/app/src/main/res/drawable/lock.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/mute.png b/app/src/main/res/drawable/mute.png new file mode 100755 index 00000000..fe29c59b Binary files /dev/null and b/app/src/main/res/drawable/mute.png differ diff --git a/app/src/main/res/drawable/slider.png b/app/src/main/res/drawable/slider.png new file mode 100755 index 00000000..d05ba141 Binary files /dev/null and b/app/src/main/res/drawable/slider.png differ diff --git a/app/src/main/res/drawable/verified.png b/app/src/main/res/drawable/verified.png new file mode 100755 index 00000000..8e523b12 Binary files /dev/null and b/app/src/main/res/drawable/verified.png differ diff --git a/app/src/main/res/drawable/video.png b/app/src/main/res/drawable/video.png new file mode 100755 index 00000000..e088392a Binary files /dev/null and b/app/src/main/res/drawable/video.png differ diff --git a/app/src/main/res/drawable/video_views.png b/app/src/main/res/drawable/video_views.png new file mode 100755 index 00000000..e3ed5af7 Binary files /dev/null and b/app/src/main/res/drawable/video_views.png differ diff --git a/app/src/main/res/drawable/vol.png b/app/src/main/res/drawable/vol.png new file mode 100755 index 00000000..036402ba Binary files /dev/null and b/app/src/main/res/drawable/vol.png differ diff --git a/app/src/main/res/drawable/zzz_adm.png b/app/src/main/res/drawable/zzz_adm.png new file mode 100755 index 00000000..3a9c108a Binary files /dev/null and b/app/src/main/res/drawable/zzz_adm.png differ diff --git a/app/src/main/res/drawable/zzz_lw.png b/app/src/main/res/drawable/zzz_lw.png new file mode 100755 index 00000000..f9ab8d3c Binary files /dev/null and b/app/src/main/res/drawable/zzz_lw.png differ diff --git a/app/src/main/res/drawable/zzz_ms.png b/app/src/main/res/drawable/zzz_ms.png new file mode 100755 index 00000000..fad973f5 Binary files /dev/null and b/app/src/main/res/drawable/zzz_ms.png differ diff --git a/app/src/main/res/drawable/zzz_qdb.png b/app/src/main/res/drawable/zzz_qdb.png new file mode 100755 index 00000000..1d442960 Binary files /dev/null and b/app/src/main/res/drawable/zzz_qdb.png differ diff --git a/app/src/main/res/drawable/zzz_rev.png b/app/src/main/res/drawable/zzz_rev.png new file mode 100755 index 00000000..c377a5f7 Binary files /dev/null and b/app/src/main/res/drawable/zzz_rev.png differ diff --git a/app/src/main/res/drawable/zzz_revl.png b/app/src/main/res/drawable/zzz_revl.png new file mode 100755 index 00000000..19524765 Binary files /dev/null and b/app/src/main/res/drawable/zzz_revl.png differ diff --git a/app/src/main/res/drawable/zzz_tesv.png b/app/src/main/res/drawable/zzz_tesv.png new file mode 100755 index 00000000..abd95775 Binary files /dev/null and b/app/src/main/res/drawable/zzz_tesv.png differ diff --git a/app/src/main/res/layout/activity_comments.xml b/app/src/main/res/layout/activity_comments.xml new file mode 100755 index 00000000..7bad2d17 --- /dev/null +++ b/app/src/main/res/layout/activity_comments.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_crash_error.xml b/app/src/main/res/layout/activity_crash_error.xml new file mode 100755 index 00000000..5c919d36 --- /dev/null +++ b/app/src/main/res/layout/activity_crash_error.xml @@ -0,0 +1,55 @@ + + + + + + + +