Merge branch 'v4'

This commit is contained in:
Rahul Kumar Patel
2021-03-01 02:59:19 +05:30
204 changed files with 6038 additions and 877 deletions

2
CHANGELOG Normal file
View File

@@ -0,0 +1,2 @@
Changelog : v4.0.1
• Bug Fixes and Improvements

55
DISCLAIMER.md Normal file
View File

@@ -0,0 +1,55 @@
# Disclaimer
### 1. Google Accounts.
You hold yourself accountable if you are to use Aurora Store with your personal Google Account, which may cause your account to be blocked/blacklisted by Google.
As per Google Play's Terms Of Service §4 and §2:
#### §4 Rights and Restrictions
> **Violation of License Terms.** If you violate any of the Content restriction provisions of the Terms, materially or repeatedly violate any of the other Terms or we are investigating you for suspected misconduct then Google may, without refund to you, terminate your access to Google Play, the Content or your Google Account and your rights under this licence will immediately terminate. We will notify you with the reason for our action unless we reasonably believe that to do so would cause Google or another person legal liability, would compromise an investigation, would compromise the operation of any Google products, services or systems, would cause harm to our users or would be in breach of the law or the direction of a legal enforcement authority. See Disabled Accounts in Section 2 for additional information.
>
> **Restrictions:** You may not:
>
> - display (in part or in whole) the Content as part of any public performance or display even if no fee is charged except (a) where such use would not constitute a copyright infringement or violate any other applicable right or (b) as specifically permitted and only in the exact manner provided.
> - sell, rent, lease, redistribute, broadcast, transmit, communicate, modify, sublicense, transfer, assign any Content to any third party including with regard to any downloads of Content that you may obtain through Google Play except as specifically permitted and only in the exact manner provided.
> - use Google Play or any Content in conjunction with any stream-ripping, stream capture or similar software to record or create a copy of any Content that is presented to you in streaming format.
> - use Content as part of any service for sharing, lending or multi-person use, or for the purpose of any other institution, except as specifically permitted and only in the exact manner provided.
> - attempt to, or assist, authorize or encourage others to circumvent, disable or defeat any of the security features or components that protect, obfuscate or otherwise restrict access to any Content or Google Play.
> - remove any watermarks, labels or other legal or proprietary notices included in any Content, or attempt to modify any Content obtained through Google Play, including any modification for the purpose of disguising or changing any indications of the ownership or source of Content.
>
> **Multiple Accounts.** If you have multiple Google Accounts with different user names, in some cases you may transfer Content out of an account and into another account, provided you are the owner of each such account and provided Google has enabled a feature of the relevant service allowing such transfers.
#### §2 Your Use of Google Play
> **Disabled Accounts.** If Google disables access to your account in accordance with the Terms (for example if you materially or repeatedly violate the Terms), you may be prevented from accessing Google Play, your account details or any files or other Content that is stored with your account. See the Help Center for more information. If you are the family manager of a family on Google Play and Google disables access to your account, your family members may lose access to family features requiring a family group, such as a family payment method, family subscriptions, or Content shared by family members. If you are a family member of a family on Google Play and Google disables your account, your family members will lose access to Content you have shared with them.
### 2. Installing **Aurora Store** from third-party sources.
You are to hold yourself responsible by installing **Aurora Store** from other sources other than our own, in which you do not blame us for malicious programs, device bricking or anything else.
The following sources are the only official links we provide:
- **F-Droid:** https://f-droid.org/en/packages/com.aurora.store/
- **AuroraOSS Website:** https://auroraoss.com/downloads.php
- **Official Telegram Channel:** https://t.me/AuroraOfficial
- **GitLab:** https://gitlab.com/AuroraOSS/AuroraStore/-/releases
### 3. Links in Aurora Store
**AuroraOSS**, makes no claim or representation regarding, and accepts no responsibility for third party websites accessible by hyperlink from the Service or websites linking to the Service. When you leave the Service, you should be aware that these Terms and our policies no longer govern.
The only links we provide are:
- **Donation links** Bitcoin, Bitcoin Cash, Etherum, BHIM-UPI, PayPal & Liberapay links
- **Repository sources** GitLab repository link
- **xda-developers** Aurora Store xda-developers thread
- **Telegram** Aurora Support group link
- **F-Droid** Aurora Store in F-Droid repository link
### 4. Content
We do not own any content within **Aurora Store**, all available content i.e. applications, information and links are provided "as is" directly from Google Play repositories/servers.
For all content on **Aurora Store**, we don't review, verify or authenticate it, and it may include inaccuracies or false information. We make no representations, warranties, or guarantees relating to the quality, suitability, truth, accuracy or completeness of any content contained in the Service. You acknowledge sole responsibility for and assume all risk arising from your use of or reliance on any content.

35
POLICY.md Normal file
View File

@@ -0,0 +1,35 @@
# Aurora Store Privacy Policy
Last Updated: **17.02.2021**
## User data sent to Google by default
The following data is mandatory to make the Service function, all of which Google needs for **Aurora Store** to be able to receive required data:
- **IP Address** which is bound to network request
- **Timezone, MCC & MNC** are stripped & replaced with a random constant
- **BUILD_CONFIG** includes the app details - package name,
version-name, version-code
- **BUILD** includes your device details, see for more info
If you choose to use your own account, the following are added to the above:
- **Account details** for Google sign-in verification
We do not store, process or sell any of the collected data above. The data is sent to and processed by Google's servers directly untouched.
All accounts being used within **Aurora Store** are saved onto the device as AAS Tokens & email pair for account verification. **No passwords** are saved to Aurora Store.
## User data sent to our server by default
The only data being sent to our server are **IP addresses**. This is to ensure the functionality and stability of our server, which detects the amount of GET requests from an IP address and if deemed them as spamming, will be banned from our server, otherwise rate-limited.
Rate-limiting lasts for an hour. If you keep going over the 20 request per hour limit, an additional hour to the rate-limiting will be added for every addtional 20 requests. So do yourself a favour and don't do it.
## Changes To This Privacy Policy
We may update our Privacy Policy from time to time. You are thus advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.
## Contact Us
If you have any questions about this Privacy Policy, please contact us at **aurora.dev@gmail.com** or via our support groups on Telegram.

83
README.md Normal file
View File

@@ -0,0 +1,83 @@
<div align="right">
<img src="https://www.gnu.org/graphics/gplv3-88x31.png" alt="GPL v3 Logo" />
</div>
<div align="center">
<img src="app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" title="AuroraStoreLogo" alt="AuroraStoreLogo" />
</div>
# Aurora Store: A Google Playstore Client
**Aurora Store** is an unofficial, FOSS client to Google's Play Store with an elegant design. Not only does Aurora Store download, update, and search for apps like the Play Store, it also empowers the user with new features.
For those concerned with privacy, **Aurora Store** does not require Google's proprietary framework (spyware?) to operate. It works perfectly fine with or without Google Play Services or [MicroG](https://microg.org/).
**Aurora Store** was originally based on Sergei Yeriomin's [Yalp store](https://github.com/yeriomin/YalpStore). **Aurora Store** v4.0 is a rewrite of version 3 in Kotlin that follows Material Design and runs on all devices running Android 4.4+. Read the roadmap page on our Wiki for more info!
## Screenshots
<img src="screenshots/account.png" height="400">
<img src="screenshots/home.png" height="400">
<img src="screenshots/sidebar.png" height="400">
<img src="screenshots/spoof.png" height="400">
## Features
- Free/Libre software — Has GPLv3 licence
- Beautiful design — Built upon latest Material Design guidelines
- Anonymous accounts — You can log in and download with anonymous accounts so you don't have to use your own account
- Personal account login — You can download purchased apps or access your wishlist by using your own Google account
- [Exodus](https://exodus-privacy.eu.org/) integration — Instantly see trackers an app is hiding in its code
## Downloads
Please only download the latest stable releases from GitLab or one of these sources:
- GitLab: [Releases](https://gitlab.com/AuroraOSS/AuroraStore/-/releases)
- AuroraOSS: [Downloads](https://auroraoss.com/downloads.php)
- AuroraOSS: [Stable](https://auroraoss.com/AuroraStore/Stable/)
- F-Droid: [Link](https://f-droid.org/en/packages/com.aurora.store/)
- Telegram: [Channel](https://t.me/AuroraOfficial)
- XDA Forum: [Thread](https://forum.xda-developers.com/android/apps-games/galaxy-playstore-alternative-t3739733)
Nightlies are released here:
- AuroraOSS: [Nightlies](https://auroraoss.com/AuroraStore/Nightly/)
`Aurora Store v3 is still in development! Bugs are to be expected! Any bug reports are appreciated.`
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/packages/com.dragons.aurora/)
## FAQ
Please visit the [GitLab Wiki](https://gitlab.com/AuroraOSS/AuroraStore/-/wikis/Home). You can also reach out to us on our support chat on [Telegram](https://t.me/AuroraSupport).
## Translations
Don't see your preffered language? Click on the widget below to help translate Aurora Store!
<a href="https://hosted.weblate.org/engage/aurora-store/">
<img src="https://hosted.weblate.org/widgets/aurora-store/-/strings/horizontal-auto.svg" alt="Translation status" />
</a>
## Project references
<details><summary>Open Source libraries Aurora Store uses</summary>
- [RX-Java](https://github.com/ReactiveX/RxJava)
- [ButterKnife](https://github.com/JakeWharton/butterknife)
- [OkHttp3](https://square.github.io/okhttp/)
- [Glide](https://github.com/bumptech/glide)
- [Fetch2](https://github.com/tonyofrancis/Fetch)
- [GPlayApi](https://gitlab.com/AuroraOSS/gplayapi)
- [PlayStoreApi-v2](https://github.com/whyorean/playstore-api-v2) (Deprecated! Used up till v3)
</details>
<details><summary>Aurora Store is based on these projects</summary>
- [YalpStore](https://github.com/yeriomin/YalpStore)
- [AppCrawler](https://github.com/Akdeniz/google-play-crawler)
- [Raccoon](https://github.com/onyxbits/raccoon4)
- [SAI](https://github.com/Aefyr/SAI)
</details>

2
TODO.md Normal file
View File

@@ -0,0 +1,2 @@
- [ ] screenshots
- [ ] ratelimiting policy

69
TermsOfService.md Normal file
View File

@@ -0,0 +1,69 @@
# **Aurora Store** TERMS OF SERVICE
Last Updated: **17.02.2021**
These terms of service ("Terms") apply to your access and use of **Aurora Store**. Please read them carefully.
## Accepting these Terms
If you access or use the Service, it means you agree to be bound by all of the terms below. So, before you use the Service, please read all of the terms. If you don't agree to all of the terms below, please do not use the Service. Also, if a term does not make sense to you, please let us know by e-mailing **auroraoss.dev@gmail.com**.
## Changes to these Terms
We reserve the right to modify these Terms at any time. For instance, we may need to change these Terms if we come out with a new feature or for some other reason.
Whenever we make changes to these Terms, the changes are effective **--.02.2021** after we post such revised Terms (indicated by revising the date at the top of these Terms) or upon your acceptance if we provide a mechanism for your immediate acceptance of the revised Terms (such as a click-through confirmation or acceptance button). It is your responsibility to check **Aurora Store** for changes to these Terms.
If you continue to use the Service after the revised Terms go into effect, then you have accepted the changes to these Terms.
## Privacy Policy
We have a privacy policy covering the collection, use, and disclosure of user information, which can be found at **https://gitlab.com/AuroraOSS/AuroraStore/POLICY.md**.
## Copyright Policy
We take intellectual property rights seriously, and by using a copyright policy in accordance with the GNU GENERAL PUBLIC LICENSE Version 3, we have adopted a policy of terminating, in appropriate circumstances and, at our sole discretion, access to the service for users who are deemed to be repeat infringers. Persons who wish to fork the Aurora Store project or copy code from the repository MUST comply with the used license. The license in use can be found here: **https://www.gnu.org/licenses/gpl-3.0.en.html**
## Disclaimer
We have a disclaimer covering the use of our Service and the content within **Aurora Store**, which can be found at **https://gitlab.com/AuroraOSS/AuroraStore/DISCLAIMER.md**
## Third-Party Services
We provide users with two major third party services. Your use of the Service may also include the use of applications that are developed or owned by a third party. Your use of such third party applications, websites, and services is governed by that party's own terms of service or privacy policies. We encourage you to read the terms and conditions and privacy policy of any third party application, website or service that you visit or use.
## Token Dispenser Server
We provide users with the ability to use anonymous accounts within **Aurora Store**, provided from our server as tokens. Your use of the Service agrees to not affect our server in any way deemed harmful or with malicious intent.
We may occasionally ask contributors to provide us with accounts. You hereby acknowledge that the accounts you make for **Aurora Store** to be given entire access to **AuroraOSS** and to be stored on our server. This does not apply to non-anonymous accounts.
## **Aurora Store** Materials
We put a lot of effort into creating the Service including, the logo and all designs, text, graphics, pictures, information and other content (excluding content within **Aurora Store**). This property is owned by us or our licensors and it is protected by open-source licenses. We grant you the right to use it.
## Unavoidable Legal Stuff
THE SERVICE AND ANY OTHER SERVICE AND CONTENT INCLUDED ON OR OTHERWISE MADE AVAILABLE TO YOU THROUGH THE SERVICE ARE PROVIDED TO YOU ON AN AS IS OR AS AVAILABLE BASIS WITHOUT ANY REPRESENTATIONS OR WARRANTIES OF ANY KIND. WE DISCLAIM ANY AND ALL WARRANTIES AND REPRESENTATIONS (EXPRESS OR IMPLIED, ORAL OR WRITTEN) WITH RESPECT TO THE SERVICE AND CONTENT INCLUDED ON OR OTHERWISE MADE AVAILABLE TO YOU THROUGH THE SERVICE WHETHER ALLEGED TO ARISE BY OPERATION OF LAW, BY REASON OF CUSTOM OR USAGE IN THE TRADE, BY COURSE OF DEALING OR OTHERWISE.
IN NO EVENT WILL **AURORA OSS** BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY SPECIAL, INDIRECT, INCIDENTAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES OF ANY KIND ARISING OUT OF OR IN CONNECTION WITH THE SERVICE OR ANY OTHER SERVICE AND/OR CONTENT INCLUDED ON OR OTHERWISE MADE AVAILABLE TO YOU THROUGH THE SERVICE, REGARDLESS OF THE FORM OF ACTION, WHETHER IN CONTRACT, TORT, STRICT LIABILITY OR OTHERWISE, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES OR ARE AWARE OF THE POSSIBILITY OF SUCH DAMAGES. OUR TOTAL LIABILITY FOR ALL CAUSES OF ACTION AND UNDER ALL THEORIES OF LIABILITY WILL BE LIMITED TO THE AMOUNT YOU PAID TO **AURORA OSS**. THIS SECTION WILL BE GIVEN FULL EFFECT EVEN IF ANY REMEDY SPECIFIED IN THIS AGREEMENT IS DEEMED TO HAVE FAILED OF ITS ESSENTIAL PURPOSE.
You agree to defend, indemnify and hold us harmless from and against any and all costs, damages, liabilities, and expenses (including attorneys' fees, costs, penalties, interest and disbursements) we incur in relation to, arising from, or for the purpose of avoiding, any claim or demand from a third party relating to your use of the Service or the use of the Service by any person using your account, including any claim that your use of the Service violates any applicable law or regulation, or the rights of any third party, and/or your violation of these Terms.
## Termination
If you breach any of these Terms, we have the right to suspend or disable your access to or use of the Service.
## Entire Agreement
These Terms constitute the entire agreement between you and **AuroraOSS** regarding the use of the Service, superseding any prior agreements between you and **AuroraOSS** relating to your use of the Service.
## Feedback
Please let us know what you think of the Service, these Terms and, in general, **Aurora Store**. When you provide us with any feedback, comments or suggestions about the Service, these Terms and, in general, **Aurora Store**, you irrevocably assign to us all of your right, title and interest in and to your feedback, comments and suggestions.
If you discover or suspect any Service security breaches, please let us know as soon as possible.
## Questions & Contact Information
Questions or comments about the Service may be directed to us at the email address **aurora.dev@gmail.com**.

View File

@@ -172,7 +172,7 @@ dependencies {
implementation "com.github.topjohnwu.libsu:core:${versions.libsu}"
//Love <3
api("com.gitlab.AuroraOSS:gplayapi:82843a04ce")
api("com.gitlab.AuroraOSS:gplayapi:7d7a0deab4")
}
Properties props = new Properties()

View File

@@ -55,17 +55,20 @@
android:name=".AuroraApplication"
android:allowBackup="true"
android:banner="@drawable/ic_launcher_background"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:killAfterRestore="false"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:restoreAnyVersion="true"
android:restoreNeedsApplication="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:targetApi="m">
<activity
android:name=".view.ui.onboarding.OnboardingActivity">
<activity android:name=".view.ui.onboarding.OnboardingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -82,11 +85,50 @@
android:name=".MainActivity"
android:launchMode="singleTask" />
<activity android:name=".view.ui.details.AppDetailsActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="market.android.com" />
<data android:path="/store/apps/details" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="play.google.com" />
<data android:path="/store/apps/details" />
</intent-filter>
</activity>
<activity android:name=".view.ui.details.DevProfileActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="play.google.com" />
<data android:path="/store/apps/dev" />
</intent-filter>
</activity>
<activity android:name=".view.ui.search.SearchSuggestionActivity" />
<activity android:name=".view.ui.search.SearchResultsActivity" />
<activity android:name=".view.ui.commons.StreamBrowseActivity" />
<activity android:name=".view.ui.sale.AppSalesActivity" />
<activity android:name=".view.ui.details.AppDetailsActivity" />
<activity android:name=".view.ui.details.ScreenshotActivity" />
<activity android:name=".view.ui.all.AppsGamesActivity" />
<activity android:name=".view.ui.commons.CategoryBrowseActivity" />
@@ -101,6 +143,8 @@
<activity android:name=".view.ui.commons.BlacklistActivity" />
<activity android:name=".view.ui.preferences.SettingsActivity" />
<activity android:name=".view.ui.about.AboutActivity" />
<activity android:name=".view.ui.commons.ExpandedStreamBrowseActivity" />
<activity android:name=".view.ui.commons.EditorStreamBrowseActivity" />
<service android:name=".data.service.NotificationService" />
<service android:name=".data.installer.InstallerService" />

View File

@@ -3,14 +3,14 @@
"id": "0",
"title": "Session installer",
"subtitle": "Session based installer for bundled/split APKs",
"description": "Best suited for devices running Android 5.0+.",
"description": "Best suited for devices running Android 5.0+",
"url": "https://developer.android.com/reference/android/content/pm/PackageInstaller.Session"
},
{
"id": "1",
"title": "Native installer",
"subtitle": "Intent based installer, available on all devices",
"description": "Best suited for devices running below Android 4.4 or OEM modified ROMs like MIUI, One-UI.",
"description": "Best suited for devices running below Android 4.4",
"url": "https://developer.android.com/reference/android/content/Intent#setDataAndType"
},
{

View File

@@ -0,0 +1,75 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.extensions
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
fun AppCompatActivity.close() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
finishAfterTransition()
} else {
finish()
}
}
fun AppCompatActivity.restart() {
val intent = intent
overridePendingTransition(0, 0)
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
finish()
overridePendingTransition(0, 0)
startActivity(intent)
}
inline fun <reified T : Activity> Activity.startActivity(): Unit =
this.startActivity(newIntent<T>())
inline fun <reified T : Activity> Activity.startActivity(flags: Int): Unit =
this.startActivity(newIntent<T>(flags))
inline fun <reified T : Activity> Activity.startActivity(extras: Bundle): Unit =
this.startActivity(newIntent<T>(extras))
inline fun <reified T : Activity> Activity.startActivity(flags: Int, extras: Bundle): Unit =
this.startActivity(newIntent<T>(flags, extras))
inline fun <reified T : Activity> Activity.startActivityForResult(requestCode: Int): Unit =
this.startActivityForResult(newIntent<T>(), requestCode)
inline fun <reified T : Activity> Activity.startActivityForResult(
requestCode: Int,
flags: Int
): Unit =
this.startActivityForResult(newIntent<T>(flags), requestCode)
inline fun <reified T : Activity> Activity.startActivityForResult(
extras: Bundle, requestCode: Int
): Unit =
this.startActivityForResult(newIntent<T>(extras), requestCode)
inline fun <reified T : Activity> Activity.startActivityForResult(
extras: Bundle, requestCode: Int, flags: Int
): Unit =
this.startActivityForResult(newIntent<T>(flags, extras), requestCode)

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.util.extensions
package com.aurora.extensions
fun <T> MutableList<T>.flushAndAdd(list: List<T>) {
clear()

View File

@@ -17,10 +17,11 @@
*
*/
package com.aurora.store.util.extensions
package com.aurora.extensions
import android.os.Build
import android.text.format.DateFormat
import java.io.PrintWriter
import java.io.StringWriter
import java.util.*
fun Long.toDate(): String {
@@ -29,18 +30,10 @@ fun Long.toDate(): String {
return DateFormat.format("dd/MM/yy", calendar).toString()
}
fun isLAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
}
fun isNAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
}
fun isPAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
}
fun isQAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
fun Throwable.stackTraceToString(): String {
val stringWriter = StringWriter(1024)
val printWriter = PrintWriter(stringWriter)
printStackTrace(printWriter)
printWriter.close()
return stringWriter.toString()
}

View File

@@ -0,0 +1,32 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.extensions
inline fun doIf(condition: Boolean?, action: () -> Unit) {
if (condition == true) action()
}
inline fun doIf(condition: () -> Boolean?, action: () -> Unit) {
if (condition() == true) action()
}
inline fun doIf(any: Any?, action: () -> Unit) {
if (any != null) action()
}

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.util.extensions
package com.aurora.extensions
import android.app.AlarmManager
import android.app.PendingIntent
@@ -25,18 +25,27 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.ShareCompat
import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.store.MainActivity
import com.aurora.store.R
import com.aurora.store.util.Log
import com.aurora.store.util.ViewUtil
import kotlin.system.exitProcess
val Context.inflater: LayoutInflater
get() = LayoutInflater.from(this)
val Context.displayMetrics: DisplayMetrics
get() = resources.displayMetrics
fun Context.browse(url: String) {
try {
@@ -82,18 +91,10 @@ fun <T> Context.open(className: Class<T>, newTask: Boolean = false) {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(
intent,
ViewUtil.getEmptyActivityBundle(this)
getEmptyActivityBundle()
)
}
fun AppCompatActivity.close() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
finishAfterTransition()
} else {
finish()
}
}
fun Context.restartApp() {
val pendingIntent = PendingIntent.getActivity(
this,
@@ -107,8 +108,23 @@ fun Context.restartApp() {
exitProcess(0)
}
fun Context.getEmptyActivityBundle(): Bundle? {
return ActivityOptionsCompat.makeCustomAnimation(
this,
android.R.anim.fade_in,
android.R.anim.fade_out
).toBundle()
}
fun Context.copyToClipBoard(data: String?) {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Download Url", data)
clipboard.setPrimaryClip(clip)
}
fun Context.getStyledAttributeColor(id: Int): Int {
val arr = obtainStyledAttributes(TypedValue().data, intArrayOf(id))
val styledAttr = arr.getColor(0, Color.WHITE)
arr.recycle()
return styledAttr
}

View File

@@ -0,0 +1,63 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.extensions
import android.content.Context
import android.content.DialogInterface
import android.graphics.drawable.ColorDrawable
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
fun Context.showDialog(@StringRes titleId: Int, @StringRes messageId: Int) {
runOnUiThread {
val backgroundColor: Int = getStyledAttributeColor(android.R.attr.colorBackground)
val builder = MaterialAlertDialogBuilder(this).apply {
setTitle(titleId)
setMessage(messageId)
setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _ -> dialog.dismiss() }
background = ColorDrawable(backgroundColor)
}.create()
builder.show()
}
}
fun Context.showDialog(title: String?, message: String?) {
runOnUiThread {
val backgroundColor: Int = getStyledAttributeColor(android.R.attr.colorBackground)
val builder = MaterialAlertDialogBuilder(this).apply {
setTitle(title)
setMessage(message)
setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _ -> dialog.dismiss() }
background = ColorDrawable(backgroundColor)
}.create()
builder.show()
}
}
fun Fragment.showDialog(@StringRes titleId: Int, @StringRes messageId: Int) {
requireContext().showDialog(titleId, messageId)
}
fun Fragment.showDialog(title: String, message: String) {
requireContext().showDialog(title, message)
}

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.util.extensions
package com.aurora.extensions
import android.graphics.Bitmap
import android.graphics.drawable.Drawable

View File

@@ -0,0 +1,42 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.extensions
import android.content.Context
import android.content.Intent
import android.os.Bundle
inline fun <reified T : Context> Context.newIntent(): Intent =
Intent(this, T::class.java)
inline fun <reified T : Context> Context.newIntent(flags: Int): Intent {
val intent = newIntent<T>()
intent.flags = flags
return intent
}
inline fun <reified T : Context> Context.newIntent(extras: Bundle): Intent =
newIntent<T>(0, extras)
inline fun <reified T : Context> Context.newIntent(flags: Int, extras: Bundle): Intent {
val intent = newIntent<T>(flags)
intent.putExtras(extras)
return intent
}

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.util.extensions
package com.aurora.extensions
import android.content.res.Resources

View File

@@ -0,0 +1,96 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.extensions
import android.annotation.SuppressLint
import android.os.Build
import java.util.*
fun isLAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
}
fun isMAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
}
fun isNAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
}
fun isOAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
}
fun isPAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
}
fun isQAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
fun isRAndAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
}
fun isMIUI(): Boolean {
return getSystemProperty("ro.miui.ui.version.name").isNotEmpty()
}
fun isHuawei(): Boolean {
return Build.MANUFACTURER.toLowerCase(Locale.getDefault()).contains("huawei")
|| Build.HARDWARE.toLowerCase(Locale.getDefault()).contains("kirin")
|| Build.HARDWARE.toLowerCase(Locale.getDefault()).contains("hi3")
}
fun getMIUIVersion(): String {
val version = getSystemProperty("ro.miui.ui.version.name")
val versionCode = getSystemProperty("ro.miui.ui.version.code")
return if (version.isNotEmpty() && versionCode.isNotEmpty())
"$version.$versionCode"
else
"unknown"
}
@SuppressLint("PrivateApi")
fun isMiuiOptimizationDisabled(): Boolean {
if ("0" == getSystemProperty("persist.sys.miui_optimization")) {
return true
} else try {
return Class.forName("android.miui.AppOpsUtils")
.getDeclaredMethod("isXOptMode")
.invoke(null) as Boolean
} catch (e: java.lang.Exception) {
return false
}
}
@SuppressLint("PrivateApi")
fun getSystemProperty(key: String): String {
return try {
Class.forName("android.os.SystemProperties")
.getDeclaredMethod("get", String::class.java)
.invoke(null, key) as String
} catch (e: Exception) {
""
}
}

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.util.extensions
package com.aurora.extensions
import android.content.Intent
import android.content.res.Configuration
@@ -33,8 +33,6 @@ import androidx.fragment.app.Fragment
import com.aurora.Constants
import com.aurora.store.R
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.ViewUtil
fun Fragment.applyTheme(
themeId: Int,
@@ -140,8 +138,7 @@ private fun AppCompatActivity.setLightStatusBar() {
private fun AppCompatActivity.setLightNavigationBar() {
var flags = window.decorView.systemUiVisibility
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window.navigationBarColor =
ViewUtil.getStyledAttribute(this, android.R.attr.colorBackground)
window.navigationBarColor = getStyledAttributeColor(android.R.attr.colorBackground)
flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
}
window.decorView.systemUiVisibility = flags

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.util.extensions
package com.aurora.extensions
import android.os.Handler
import android.os.Looper
@@ -25,17 +25,10 @@ import android.os.Looper
fun runAsync(action: () -> Unit) = Thread(Runnable(action)).start()
fun runOnUiThread(action: () -> Unit) {
if (isMainLooperAlive()) {
action()
} else {
Handler(Looper.getMainLooper()).post(Runnable(action))
when {
isMainThread() -> action.invoke()
else -> Handler(Looper.getMainLooper()).post(Runnable(action))
}
}
fun runDelayed(delayMillis: Long, action: () -> Unit) =
Handler().postDelayed(Runnable(action), delayMillis)
fun runDelayedOnUiThread(delayMillis: Long, action: () -> Unit) =
Handler(Looper.getMainLooper()).postDelayed(Runnable(action), delayMillis)
private fun isMainLooperAlive() = Looper.myLooper() == Looper.getMainLooper()
private fun isMainThread() = Looper.myLooper() == Looper.getMainLooper()

View File

@@ -17,20 +17,32 @@
*
*/
package com.aurora.store.util.extensions
package com.aurora.extensions
import android.content.Context
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
fun Context.toast(text: CharSequence): Toast =
Toast.makeText(this, text, Toast.LENGTH_SHORT).apply { show() }
fun Context.longToast(text: CharSequence): Toast =
Toast.makeText(this, text, Toast.LENGTH_LONG).apply { show() }
fun Context.toast(@StringRes resId: Int): Toast =
fun Context.toast(resId: Int) = runOnUiThread {
Toast.makeText(this, resId, Toast.LENGTH_SHORT).apply { show() }
}
fun Context.longToast(@StringRes resId: Int): Toast =
fun Context.toast(text: CharSequence) = runOnUiThread {
Toast.makeText(this, text, Toast.LENGTH_SHORT).apply { show() }
}
fun Context.toastLong(resId: Int) = runOnUiThread {
Toast.makeText(this, resId, Toast.LENGTH_LONG).apply { show() }
}
fun Context.toastLong(text: CharSequence) = runOnUiThread {
Toast.makeText(this, text, Toast.LENGTH_LONG).apply { show() }
}
fun Fragment.toast(resId: Int) = requireContext().toast(resId)
fun Fragment.toast(text: CharSequence) = requireContext().toast(text)
fun Fragment.toastLong(resId: Int) = requireContext().toastLong(resId)
fun Fragment.toastLong(text: CharSequence) = requireContext().toastLong(text)

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.util.extensions
package com.aurora.extensions
import android.content.Context
import android.view.View

View File

@@ -36,6 +36,10 @@ class AuroraApplication : MultiDexApplication() {
private lateinit var fetch: Fetch
private lateinit var packageManagerReceiver: PackageManagerReceiver
companion object{
val enqueuedInstalls: MutableSet<String> = mutableSetOf()
}
override fun onCreate() {
super.onCreate()

View File

@@ -21,7 +21,10 @@ package com.aurora.store
import android.Manifest
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -36,15 +39,14 @@ import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.Navigation
import androidx.navigation.ui.NavigationUI
import com.aurora.extensions.getEmptyActivityBundle
import com.aurora.extensions.getStyledAttributeColor
import com.aurora.extensions.load
import com.aurora.extensions.open
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.databinding.ActivityMainBinding
import com.aurora.store.util.Log
import com.aurora.store.util.ViewUtil
import com.aurora.store.util.ViewUtil.getStyledAttribute
import com.aurora.store.util.extensions.isQAndAbove
import com.aurora.store.util.extensions.load
import com.aurora.store.util.extensions.open
import com.aurora.store.view.ui.about.AboutActivity
import com.aurora.store.view.ui.account.AccountActivity
import com.aurora.store.view.ui.all.AppsGamesActivity
@@ -68,6 +70,18 @@ class MainActivity : BaseActivity() {
private var lastBackPressed = 0L
override fun onConnected() {
hideNetworkConnectivitySheet()
}
override fun onDisconnected() {
showNetworkConnectivitySheet()
}
override fun onReconnected() {
}
companion object {
@JvmStatic
private fun matchDestination(
@@ -97,16 +111,20 @@ class MainActivity : BaseActivity() {
attachSearch()
checkPermission()
checkStoragePermission()
}
private fun checkPermission() = runWithPermissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) {
Log.i("External Storage Access Available")
Log.i("Required permissions available")
}
if (isQAndAbove()) {
//pickExternalFileDir()
private fun checkStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager())
startActivity(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION))
}
}
@@ -116,8 +134,7 @@ class MainActivity : BaseActivity() {
}
B.viewToolbar.imgActionSecondary.setOnClickListener {
val userAppIntent = Intent(this, DownloadActivity::class.java)
startActivity(userAppIntent, ViewUtil.getEmptyActivityBundle(this))
open(DownloadActivity::class.java)
}
}
@@ -125,7 +142,7 @@ class MainActivity : BaseActivity() {
B.searchFab.setOnClickListener {
startActivity(
Intent(this, SearchSuggestionActivity::class.java),
ViewUtil.getEmptyActivityBundle(this)
getEmptyActivityBundle()
)
}
}
@@ -134,7 +151,7 @@ class MainActivity : BaseActivity() {
val bottomNavigationView: BottomNavigationView = B.navView
navController = Navigation.findNavController(this, R.id.nav_host_fragment)
val backGroundColor = getStyledAttribute(this, android.R.attr.colorBackground)
val backGroundColor = getStyledAttributeColor(android.R.attr.colorBackground)
bottomNavigationView.setBackgroundColor(ColorUtils.setAlphaComponent(backGroundColor, 245))
@@ -215,18 +232,6 @@ class MainActivity : BaseActivity() {
}
}
override fun onConnected() {
hideNetworkConnectivitySheet()
}
override fun onDisconnected() {
showNetworkConnectivitySheet()
}
override fun onReconnected() {
}
private fun pickExternalFileDir() {
val getContentIntent = Intent(Intent.ACTION_GET_CONTENT)
getContentIntent.type = "*/*"

View File

@@ -20,12 +20,37 @@
package com.aurora.store.data.event
sealed class BusEvent {
data class InstallEvent(var packageName: String, var error: String = String()) : BusEvent()
data class UninstallEvent(var packageName: String, var error: String = String()) : BusEvent()
data class Blacklisted(var packageName: String, var error: String = String()) : BusEvent()
data class InstallEvent(
var packageName: String,
var extra: String? = ""
) : BusEvent()
data class UninstallEvent(
var packageName: String,
var extra: String? = ""
) : BusEvent()
data class Blacklisted(
var packageName: String,
var error: String? = ""
) : BusEvent()
data class GoogleAAS(
var success: Boolean,
var email: String = String(),
var aasToken: String = String()
) : BusEvent()
}
sealed class InstallerEvent {
data class Success(
var packageName: String? = "",
var extra: String? = ""
) : InstallerEvent()
data class Failed(
var packageName: String? = "",
var error: String? = "",
var extra: String? = ""
) : InstallerEvent()
}

View File

@@ -25,9 +25,7 @@ import com.aurora.store.data.SingletonHolder
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_INSTALLER_ID
open class AppInstaller private constructor(var context: Context) {
companion object : SingletonHolder<AppInstaller, Context>(::AppInstaller)
open class AppInstaller constructor(var context: Context) {
fun getPreferredInstaller(): IInstaller {
val prefValue = Preferences.getInteger(

View File

@@ -24,23 +24,22 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.core.content.FileProvider
import com.aurora.store.AuroraApplication
import com.aurora.store.BuildConfig
import java.io.File
abstract class InstallerBase(protected var context: Context) : IInstaller {
private val enqueuedInstalls: MutableSet<String> = mutableSetOf()
override fun clearQueue() {
enqueuedInstalls.clear()
AuroraApplication.enqueuedInstalls.clear()
}
override fun isAlreadyQueued(packageName: String): Boolean {
return enqueuedInstalls.contains(packageName)
return AuroraApplication.enqueuedInstalls.contains(packageName)
}
override fun removeFromInstallQueue(packageName: String) {
enqueuedInstalls.remove(packageName)
AuroraApplication.enqueuedInstalls.remove(packageName)
}
override fun uninstall(packageName: String) {

View File

@@ -25,52 +25,69 @@ import android.content.pm.PackageInstaller
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import org.apache.commons.lang3.StringUtils
import com.aurora.store.R
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.util.Log
import org.greenrobot.eventbus.EventBus
class InstallerService : Service() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -69)
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
//Send broadcast for the installation status of the package
sendStatusBroadcast(status, packageName)
//Launch user confirmation activity
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
confirmationIntent!!.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
confirmationIntent.putExtra(
Intent.EXTRA_INSTALLER_PACKAGE_NAME,
"com.android.vending"
)
confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
startActivity(confirmationIntent)
} catch (e: Exception) {
sendStatusBroadcast(PackageInstaller.STATUS_FAILURE, packageName)
}
val extra = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> promptUser(intent)
else -> postStatus(status, packageName, extra)
}
stopSelf()
return START_NOT_STICKY
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private fun sendStatusBroadcast(status: Int, packageName: String?) {
if (StringUtils.isNotEmpty(packageName)) {
val statusIntent = Intent(ACTION_SESSION_INSTALLER)
statusIntent.putExtra(PackageInstaller.EXTRA_STATUS, status)
statusIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName)
sendBroadcast(statusIntent)
private fun promptUser(intent: Intent) {
val confirmationIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT)
confirmationIntent?.let {
it.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
it.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, "com.android.vending")
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
startActivity(it)
} catch (e: Exception) {
Log.e(e.message)
}
}
}
private fun postStatus(status: Int, packageName: String?, extra: String?) {
when (status) {
PackageInstaller.STATUS_SUCCESS -> {
EventBus.getDefault().post(InstallerEvent.Success(packageName, "Success"))
}
else -> {
val errorString = getErrorString(status)
Log.e("$packageName : $errorString")
EventBus.getDefault().post(InstallerEvent.Failed(packageName, errorString, extra))
}
}
}
private fun getErrorString(status: Int): String {
return when (status) {
PackageInstaller.STATUS_FAILURE_ABORTED -> getString(R.string.installer_status_user_action)
PackageInstaller.STATUS_FAILURE_BLOCKED -> getString(R.string.installer_status_failure_blocked)
PackageInstaller.STATUS_FAILURE_CONFLICT -> getString(R.string.installer_status_failure_conflict)
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> getString(R.string.installer_status_failure_incompatible)
PackageInstaller.STATUS_FAILURE_INVALID -> getString(R.string.installer_status_failure_invalid)
PackageInstaller.STATUS_FAILURE_STORAGE -> getString(R.string.installer_status_failure_storage)
else -> getString(R.string.installer_status_failure)
}
}
override fun onBind(intent: Intent): IBinder? {
return null
}
companion object {
private const val ACTION_SESSION_INSTALLER = "ACTION_SESSION_INSTALLER"
}
}

View File

@@ -20,11 +20,12 @@
package com.aurora.store.data.installer
import android.content.Context
import com.aurora.extensions.isLAndAbove
import com.aurora.store.R
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.util.Log
import com.aurora.store.util.extensions.isLAndAbove
import com.aurora.store.util.extensions.toast
import com.topjohnwu.superuser.Shell
import org.greenrobot.eventbus.EventBus
import java.io.File
import java.util.regex.Pattern
@@ -50,7 +51,12 @@ class RootInstaller(context: Context) : InstallerBase(context) {
xInstallLegacy(packageName, it)
}
} else {
context.toast(context.getString(R.string.installer_root_unavailable))
val event = InstallerEvent.Failed(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_root_unavailable)
)
EventBus.getDefault().post(event)
Log.e(" >>>>>>>>>>>>>>>>>>>>>>>>>> NO ROOT ACCESS <<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
}
}
@@ -80,12 +86,34 @@ class RootInstaller(context: Context) : InstallerBase(context) {
.exec()
}
Shell.su("pm install-commit $sessionId").exec()
val shellResult = Shell.su("pm install-commit $sessionId").exec()
if (!shellResult.isSuccess) {
removeFromInstallQueue(packageName)
val event = InstallerEvent.Failed(
packageName,
context.getString(R.string.installer_status_failure),
parseError(shellResult)
)
EventBus.getDefault().post(event)
}
} else {
removeFromInstallQueue(packageName)
val event = InstallerEvent.Failed(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_root_unavailable)
)
EventBus.getDefault().post(event)
}
} else {
removeFromInstallQueue(packageName)
val event = InstallerEvent.Failed(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_status_failure_session)
)
EventBus.getDefault().post(event)
}
}
@@ -96,4 +124,8 @@ class RootInstaller(context: Context) : InstallerBase(context) {
removeFromInstallQueue(packageName)
}
}
private fun parseError(result: Shell.Result): String {
return result.err.joinToString(separator = "\n")
}
}

View File

@@ -28,10 +28,15 @@ import android.os.Build
import android.os.IBinder
import android.os.RemoteException
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import com.aurora.services.IPrivilegedCallback
import com.aurora.services.IPrivilegedService
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil
import org.greenrobot.eventbus.EventBus
import java.io.File
class ServiceInstaller(context: Context) : InstallerBase(context) {
@@ -44,47 +49,81 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun install(packageName: String, files: List<Any>) {
if (isAlreadyQueued(packageName)) {
Log.i("$packageName already queued")
} else {
Log.i("Received service install request for $packageName")
val uriList = files.map {
when (it) {
is File -> getUri(it)
is String -> getUri(File(it))
else -> {
throw Exception("Invalid data, expecting listOf() File or String")
}
}
when {
isAlreadyQueued(packageName) -> {
Log.i("$packageName already queued")
}
xInstall(packageName, uriList)
PackageUtil.isInstalled(context, PRIVILEGED_EXTENSION_PACKAGE_NAME) -> {
Log.i("Received service install request for $packageName")
val uriList = files.map {
when (it) {
is File -> getUri(it)
is String -> getUri(File(it))
else -> {
throw Exception("Invalid data, expecting listOf() File or String")
}
}
}
xInstall(packageName, uriList)
}
else -> {
val event = InstallerEvent.Failed(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_service_unavailable)
)
EventBus.getDefault().post(event)
}
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun xInstall(packageName: String, uriList: List<Uri>) {
val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
val service = IPrivilegedService.Stub.asInterface(binder)
val callback = object : IPrivilegedCallback.Stub() {
override fun handleResult(packageName: String, returnCode: Int) {
removeFromInstallQueue(packageName)
}
}
try {
service.installSplitPackage(
if (service.hasPrivilegedPermissions()) {
Log.i(context.getString(R.string.installer_service_available))
val callback = object : IPrivilegedCallback.Stub() {
override fun handleResult(packageName: String, returnCode: Int) {
Log.e("$packageName : $returnCode")
removeFromInstallQueue(packageName)
}
}
try {
service.installSplitPackage(
packageName,
uriList,
ACTION_INSTALL_REPLACE_EXISTING,
BuildConfig.APPLICATION_ID,
callback
)
} catch (e: RemoteException) {
removeFromInstallQueue(packageName)
EventBus
.getDefault()
.post(
InstallerEvent.Failed(
packageName,
e.localizedMessage,
e.stackTraceToString()
)
)
Log.e("Failed to connect Aurora Services")
}
} else {
Log.e(context.getString(R.string.installer_service_misconfigured))
val event = InstallerEvent.Failed(
packageName,
uriList,
ACTION_INSTALL_REPLACE_EXISTING,
BuildConfig.APPLICATION_ID,
callback
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_service_misconfigured)
)
} catch (e: RemoteException) {
removeFromInstallQueue(packageName)
Log.e("Failed to connect Aurora Services")
EventBus.getDefault().post(event)
}
}
@@ -103,4 +142,20 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
Context.BIND_AUTO_CREATE
)
}
override fun getUri(file: File): Uri {
val uri = FileProvider.getUriForFile(
context,
"${BuildConfig.APPLICATION_ID}.fileProvider",
file
)
context.grantUriPermission(
PRIVILEGED_EXTENSION_PACKAGE_NAME,
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
return uri
}
}

View File

@@ -27,9 +27,12 @@ import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import com.aurora.extensions.isNAndAbove
import com.aurora.store.BuildConfig
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.util.Log
import org.apache.commons.io.IOUtils
import org.greenrobot.eventbus.EventBus
import java.io.File
class SessionInstaller(context: Context) : InstallerBase(context) {
@@ -57,18 +60,22 @@ class SessionInstaller(context: Context) : InstallerBase(context) {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun xInstall(packageName: String, uriList: List<Uri>) {
val packageInstaller = context.packageManager.packageInstaller
val sessionParams = SessionParams(SessionParams.MODE_FULL_INSTALL)
val sessionParams = SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
setAppPackageName(packageName)
if (isNAndAbove()) {
setOriginatingUid(android.os.Process.myUid())
}
}
val sessionId = packageInstaller.createSession(sessionParams)
val session = packageInstaller.openSession(sessionId)
try {
Log.i("Writing splits to session for $packageName")
var apkId = 1
for (uri in uriList) {
val inputStream = context.contentResolver.openInputStream(uri)
val outputStream = session.openWrite(
"${packageName}_${apkId++}",
"${packageName}_${System.currentTimeMillis()}",
0,
-1
)
@@ -81,11 +88,11 @@ class SessionInstaller(context: Context) : InstallerBase(context) {
IOUtils.close(outputStream)
}
val intent = Intent(context, InstallerService::class.java)
val callBackIntent = Intent(context, InstallerService::class.java)
val pendingIntent = PendingIntent.getService(
context,
sessionId,
intent,
callBackIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
@@ -95,6 +102,12 @@ class SessionInstaller(context: Context) : InstallerBase(context) {
} catch (e: Exception) {
session.abandon()
removeFromInstallQueue(packageName)
val event = InstallerEvent.Failed(
packageName,
e.localizedMessage,
e.stackTraceToString()
)
EventBus.getDefault().post(event)
Log.e("Failed to install $packageName : %s", e.message)
}
}
@@ -106,13 +119,11 @@ class SessionInstaller(context: Context) : InstallerBase(context) {
file
)
uri.apply {
context.grantUriPermission(
BuildConfig.APPLICATION_ID,
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
context.grantUriPermission(
BuildConfig.APPLICATION_ID,
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
return uri
}

View File

@@ -0,0 +1,39 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.data.model
data class AppPermission(
var name: String,
var permission: String,
var raw: String,
var flags: Int,
var icon: Int
) {
override fun equals(other: Any?): Boolean {
return when (other) {
is AppPermission -> other.permission == permission
else -> false
}
}
override fun hashCode(): Int {
return permission.hashCode()
}
}

View File

@@ -20,16 +20,15 @@
package com.aurora.store.data.providers
import android.content.Context
import com.aurora.store.data.SingletonHolder
import com.aurora.store.util.Preferences
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.lang.reflect.Modifier
import java.util.*
class SpoofProvider private constructor(var context: Context) {
class SpoofProvider constructor(var context: Context) {
companion object : SingletonHolder<SpoofProvider, Context>(::SpoofProvider) {
companion object {
const val LOCALE_SPOOF_ENABLED = "LOCALE_SPOOF_ENABLED"
const val LOCALE_SPOOF_LANG = "LOCALE_SPOOF_LANG"
const val LOCALE_SPOOF_COUNTRY = "LOCALE_SPOOF_COUNTRY"

View File

@@ -28,6 +28,7 @@ import com.aurora.store.data.event.BusEvent.UninstallEvent
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.util.PathUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.isExternalStorageEnable
import org.greenrobot.eventbus.EventBus
import java.io.File
@@ -39,19 +40,20 @@ open class PackageManagerReceiver : BroadcastReceiver() {
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
EventBus.getDefault()
.post(InstallEvent(packageName, ""))
//Clear installation queue
AppInstaller.with(context)
.getPreferredInstaller()
.removeFromInstallQueue(packageName)
EventBus.getDefault().post(InstallEvent(packageName, ""))
}
Intent.ACTION_PACKAGE_REMOVED -> {
EventBus.getDefault().post(UninstallEvent(packageName, ""))
}
Intent.ACTION_PACKAGE_REMOVED -> EventBus.getDefault()
.post(UninstallEvent(packageName, ""))
}
clearNotification(context, packageName)
//Clear installation queue
AppInstaller(context)
.getPreferredInstaller()
.removeFromInstallQueue(packageName)
//clearNotification(context, packageName)
val isAutoDeleteAPKEnabled = Preferences.getBoolean(
context,

View File

@@ -29,9 +29,12 @@ import android.util.ArrayMap
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.aurora.Constants
import com.aurora.extensions.getStyledAttributeColor
import com.aurora.extensions.isLAndAbove
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.data.downloader.DownloadManager
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.receiver.DownloadCancelReceiver
import com.aurora.store.data.receiver.DownloadPauseReceiver
@@ -39,13 +42,14 @@ import com.aurora.store.data.receiver.DownloadResumeReceiver
import com.aurora.store.data.receiver.InstallReceiver
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.Log
import com.aurora.store.util.extensions.isLAndAbove
import com.aurora.store.view.ui.details.AppDetailsActivity
import com.aurora.store.view.ui.downloads.DownloadActivity
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.tonyodev.fetch2.*
import org.apache.commons.lang3.StringUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import java.lang.reflect.Modifier
import java.util.*
@@ -83,6 +87,8 @@ class NotificationService : Service() {
Log.i("Notification Service Started")
EventBus.getDefault().register(this)
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
//Create Notification Channels : General & Alert
@@ -97,9 +103,6 @@ class NotificationService : Service() {
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
showNotification(groupId, download, fetchGroup)
if (fetchGroup.groupDownloadProgress == 100) {
install(download.tag!!, fetchGroup.downloads)
}
}
override fun onError(
@@ -214,8 +217,8 @@ class NotificationService : Service() {
}
val progress = fetchGroup.groupDownloadProgress
val progressBigText = NotificationCompat.BigTextStyle()
when (status) {
when (status) {
Status.QUEUED -> {
builder.setProgress(100, 0, true)
progressBigText.bigText(getString(R.string.download_queued))
@@ -308,7 +311,6 @@ class NotificationService : Service() {
app.id,
builder.build()
)
//}
}
private fun getPauseIntent(groupId: Int): PendingIntent {
@@ -356,9 +358,28 @@ class NotificationService : Service() {
)
}
@Subscribe()
fun onEventMainThread(event: Any) {
when (event) {
is InstallerEvent.Success -> {
val app = appMap[event.packageName.hashCode()]
if (app != null)
notifyInstallationStatus(app, event.extra)
}
is InstallerEvent.Failed -> {
val app = appMap[event.packageName.hashCode()]
if (app != null)
notifyInstallationStatus(app, event.error)
}
else -> {
}
}
}
@Synchronized
private fun install(packageName: String, files: List<Download>) {
AppInstaller.with(this)
AppInstaller(this)
.getPreferredInstaller()
.install(
packageName,
@@ -370,9 +391,25 @@ class NotificationService : Service() {
)
}
private fun notifyInstallationStatus(app: App, status: String?) {
val builder = NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_ALERT)
builder.color = getStyledAttributeColor(R.attr.colorAccent)
builder.setSmallIcon(R.drawable.ic_install)
builder.setContentTitle(app.displayName)
builder.setContentText(status)
builder.setSubText(app.packageName)
notificationManager.notify(
app.packageName,
app.id,
builder.build()
)
}
override fun onDestroy() {
Log.i("Notification Service Stopped")
fetch.removeListener(fetchListener)
EventBus.getDefault().unregister(this);
super.onDestroy()
}
}

View File

@@ -52,7 +52,7 @@ class AC2DMTask {
.response()
return response.third.fold(success = {
Util.parseResponse(String(it))
AC2DMUtil.parseResponse(String(it))
}, failure = {
mapOf()
})

View File

@@ -25,7 +25,7 @@ import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Util {
public class AC2DMUtil {
public static Map<String, String> parseResponse(String response) {
Map<String, String> keyValueMap = new HashMap<String, String>();

View File

@@ -22,12 +22,9 @@ package com.aurora.store.util
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import com.aurora.store.BuildConfig
import com.aurora.store.util.extensions.isLAndAbove
import com.aurora.extensions.isLAndAbove
import org.apache.commons.io.IOUtils
import java.io.File
import java.io.FileOutputStream
@@ -111,12 +108,4 @@ class ApkCopier(private val context: Context, private val packageName: String) {
Log.e("ApkCopier : %s", e.message)
}
}
fun getUri(file: File): Uri {
return FileProvider.getUriForFile(
context,
"${BuildConfig.APPLICATION_ID}.fileProvider",
file
)
}
}

View File

@@ -21,7 +21,7 @@ package com.aurora.store.util
import android.content.Context
import android.content.pm.PackageManager
import com.aurora.store.util.extensions.isPAndAbove
import com.aurora.extensions.isPAndAbove
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.cert.CertificateFactory
@@ -35,22 +35,24 @@ object CertUtil {
private fun getX509Certificates(
context: Context,
packageName: String
): List<X509Certificate?> {
val certificates: MutableList<X509Certificate?> = mutableListOf()
): List<X509Certificate> {
val certificates: MutableList<X509Certificate> = mutableListOf()
val packageManager = context.applicationContext.packageManager
try {
val packageInfo = if (isPAndAbove())
val packageInfo = if (isPAndAbove()) {
packageManager.getPackageInfo(
packageName,
PackageManager.GET_SIGNING_CERTIFICATES
)
else
}
else {
packageManager.getPackageInfo(
packageName,
PackageManager.GET_SIGNATURES
)
}
val certificateFactory = CertificateFactory.getInstance("X509")
@@ -63,7 +65,7 @@ object CertUtil {
)
}
} else {
for (i in 0..packageInfo.signatures.size) {
for (i in packageInfo.signatures.indices) {
val bytes = packageInfo.signatures[i].toByteArray()
val inStream: InputStream = ByteArrayInputStream(bytes)
certificates.add(
@@ -72,7 +74,7 @@ object CertUtil {
}
}
} catch (e: Exception) {
Log.e(e.message)
}
return certificates
@@ -85,13 +87,10 @@ object CertUtil {
false
else {
val cert = certificates[0]
if (cert != null) {
if (cert.subjectDN != null) {
val DN = cert.subjectDN.name.toUpperCase(Locale.getDefault())
DN.contains(FDROID) || DN.contains(GUARDIAN)
} else {
false
}
if (cert.subjectDN != null) {
val DN = cert.subjectDN.name.toUpperCase(Locale.getDefault())
DN.contains(FDROID) || DN.contains(GUARDIAN)
} else {
false
}

View File

@@ -21,8 +21,6 @@ package com.aurora.store.util
import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.core.app.ActivityOptionsCompat
import com.aurora.store.R
import java.text.DecimalFormat
import java.util.*
@@ -37,6 +35,7 @@ object CommonUtil {
Pair(6, " MB"),
Pair(9, " GB")
)
private val diPrefixes: Map<Int, String> = hashMapOf(
Pair(1, ""),
Pair(3, " K"),
@@ -134,14 +133,6 @@ object CommonUtil {
}
}
fun getEmptyActivityBundle(context: Context): Bundle? {
return ActivityOptionsCompat.makeCustomAnimation(
context,
android.R.anim.fade_in,
android.R.anim.fade_out
).toBundle()
}
fun cleanupInstallationSessions(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val packageInstaller = context.packageManager.packageInstaller

View File

@@ -51,6 +51,19 @@ object PackageUtil {
}
}
fun getInstalledVersion(context: Context, packageName: String): String {
return try {
val packageInfo = getPackageInfo(context, packageName)
if (packageInfo != null) {
"${packageInfo.versionName}.${packageInfo.versionCode}"
} else {
""
}
} catch (e: PackageManager.NameNotFoundException) {
""
}
}
fun isTv(context: Context): Boolean {
val uiMode = context.resources.configuration.uiMode
return uiMode and Configuration.UI_MODE_TYPE_MASK == Configuration.UI_MODE_TYPE_TELEVISION
@@ -111,7 +124,7 @@ object PackageUtil {
}
if (isFdroidFilterEnabled) {
packageInfoList
packageInfoList = packageInfoList
.filter {
val packageInstaller = packageManager.getInstallerPackageName(it.packageName)
packageInstaller != "org.fdroid.fdroid.privileged"

View File

@@ -21,9 +21,9 @@ package com.aurora.store.util
import android.content.Context
import android.os.Environment
import com.aurora.extensions.isLAndAbove
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.File
import com.aurora.store.util.extensions.isLAndAbove
fun Context.getInternalBaseDirectory(): String {
return filesDir.path
@@ -32,14 +32,18 @@ fun Context.getInternalBaseDirectory(): String {
object PathUtil {
private fun getDownloadDirectory(context: Context): String {
return if (isLAndAbove())
context.getInternalBaseDirectory() + "/Downloads"
else
return if (isLAndAbove()) {
if (context.isExternalStorageEnable())
getExternalPath()
else
context.getInternalBaseDirectory() + "/Downloads"
} else {
getExternalPath()
}
}
fun getPackageDirectory(context: Context, packageName: String): String {
return getDownloadDirectory(context) + "/$packageName"
return getDownloadDirectory(context) + "/Downloads/$packageName"
}
private fun getVersionDirectory(
@@ -59,11 +63,11 @@ object PathUtil {
}
fun getExternalPath(): String {
return Environment.getExternalStorageDirectory().toString() + "/Aurora/"
return Environment.getExternalStorageDirectory().toString() + "/Aurora/Store"
}
fun getBaseCopyDirectory(): String {
return "${getExternalPath()}/files/export/"
return "${getExternalPath()}/Exports/"
}
private fun getObbDownloadPath(context: Context, app: App): String {
@@ -75,4 +79,8 @@ object PathUtil {
val obbDir = getObbDownloadPath(context, app)
return "$obbDir/${file.name}"
}
}
fun Context.isExternalStorageEnable(): Boolean {
return Preferences.getBoolean(this, Preferences.PREFERENCE_DOWNLOAD_EXTERNAL)
}

View File

@@ -43,6 +43,7 @@ object Preferences {
const val INSTALLATION_ABANDON_SESSION = "INSTALLATION_ABANDON_SESSION"
const val PREFERENCE_DOWNLOAD_ACTIVE = "PREFERENCE_DOWNLOAD_ACTIVE"
const val PREFERENCE_DOWNLOAD_EXTERNAL = "PREFERENCE_DOWNLOAD_EXTERNAL"
const val PREFERENCE_DOWNLOAD_WIFI = "PREFERENCE_DOWNLOAD_WIFI"
@@ -91,26 +92,17 @@ object Preferences {
}
}
fun Context.save(key: String, value: Int) {
Preferences.putInteger(this, key, value)
}
/*Preference Extensions*/
fun Fragment.save(key: String, value: Int) {
Preferences.putInteger(requireContext(), key, value)
}
fun Context.save(key: String, value: Int) = Preferences.putInteger(this, key, value)
fun Context.save(key: String, value: Boolean) {
Preferences.putBoolean(this, key, value)
}
fun Context.save(key: String, value: Boolean) = Preferences.putBoolean(this, key, value)
fun Fragment.save(key: String, value: Boolean) {
Preferences.putBoolean(requireContext(), key, value)
}
fun Context.save(key: String, value: String) = Preferences.putString(this, key, value)
fun Context.save(key: String, value: String) {
Preferences.putString(this, key, value)
}
fun Fragment.save(key: String, value: String) {
Preferences.putString(requireContext(), key, value)
}
fun Fragment.save(key: String, value: Int) = requireContext().save(key, value)
fun Fragment.save(key: String, value: Boolean) = requireContext().save(key, value)
fun Fragment.save(key: String, value: String) = requireContext().save(key, value)

View File

@@ -1,71 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.util
import android.app.Activity
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.WindowManager
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityOptionsCompat
object ViewUtil {
fun getEmptyActivityBundle(context: Context?): Bundle? {
return ActivityOptionsCompat.makeCustomAnimation(
context!!,
android.R.anim.fade_in,
android.R.anim.fade_out
).toBundle()
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun configureActivityLayout(activity: Activity, isLight: Boolean) {
val window = activity.window
val params = window.attributes
params.flags = params.flags and WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
window.attributes = params
window.statusBarColor = Color.TRANSPARENT
setFullScreenLightStatusBar(activity, isLight)
}
private fun setFullScreenLightStatusBar(activity: Activity, isLight: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
var flags = activity.window.decorView.systemUiVisibility
if (isLight)
flags = flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
flags = flags or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
flags = flags or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
activity.window.decorView.systemUiVisibility = flags
}
}
fun getStyledAttribute(context: Context, styleID: Int): Int {
val arr = context.obtainStyledAttributes(TypedValue().data, intArrayOf(styleID))
val styledColor = arr.getColor(0, Color.WHITE)
arr.recycle()
return styledColor
}
}

View File

@@ -0,0 +1,166 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.custom.layouts
import android.annotation.TargetApi
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PermissionGroupInfo
import android.content.pm.PermissionInfo
import android.content.res.Resources.NotFoundException
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.aurora.extensions.showDialog
import com.aurora.store.R
import java.util.*
class PermissionGroup : LinearLayout {
private lateinit var permissionGroupInfo: PermissionGroupInfo
private lateinit var packageManager: PackageManager
private val permissionMap: MutableMap<String, String> = HashMap()
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init()
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
init()
}
private fun init() {
inflate(context, R.layout.view_permission, this)
packageManager = context.packageManager
}
fun setPermissionGroupInfo(permissionGroupInfo: PermissionGroupInfo) {
this.permissionGroupInfo = permissionGroupInfo
val imageView = findViewById<ImageView>(R.id.img)
imageView.setImageDrawable(getPermissionGroupIcon(permissionGroupInfo))
//imageView.setColorFilter(getContext().getStyledAttributeColor(imageView.getContext(), android.R.attr.colorAccent));
}
fun addPermission(permissionInfo: PermissionInfo) {
val title = permissionInfo.loadLabel(packageManager)
val description = permissionInfo.loadDescription(packageManager)
permissionMap[getReadableLabel(title.toString(), permissionInfo.packageName)] =
if (description.isNullOrEmpty())
"No description"
else
description.toString()
val permissionLabels: List<String> = ArrayList(permissionMap.keys)
val permissionLabelsView = findViewById<LinearLayout>(R.id.permission_labels)
permissionLabelsView.removeAllViews()
permissionLabels
.filter { it.isNotEmpty() }
.sortedBy { it }
.forEach {
addPermissionLabel(
permissionLabelsView,
it,
permissionMap[it]
)
}
}
private fun addPermissionLabel(
permissionLabelsView: LinearLayout,
label: String,
description: String?
) {
val textView = TextView(context)
textView.text = label
textView.setOnClickListener {
var title: String = permissionGroupInfo.loadLabel(packageManager).toString()
if (title.contains("UNDEFINED")) {
title = "Android"
}
title = title.capitalize(Locale.getDefault())
context.showDialog(title, description)
}
permissionLabelsView.addView(textView)
}
private fun getPermissionGroupIcon(permissionGroupInfo: PermissionGroupInfo?): Drawable? {
return try {
ContextCompat.getDrawable(context, permissionGroupInfo!!.icon)
} catch (e: NotFoundException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1)
permissionGroupInfo!!.loadUnbadgedIcon(packageManager)
else
permissionGroupInfo!!.loadIcon(packageManager)
}
}
private fun getReadableLabel(label: String, packageName: String): String {
val prefixes: MutableList<String> = mutableListOf(
"android",
packageName
)
if (label.contains("UNDEFINED")) {
return "Android"
}
prefixes
.map { "$it.permission." }
.forEach {
if (label.startsWith(it)) {
return it.replace(it, "")
.replace("_", " ")
.toLowerCase(Locale.getDefault())
.capitalize(Locale.getDefault())
}
}
return label.capitalize(Locale.getDefault())
}
}

View File

@@ -17,16 +17,22 @@
*
*/
package com.aurora.store.view.custom
package com.aurora.store.view.custom.layouts.button
import android.content.Context
import android.content.res.ColorStateList
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.widget.RelativeLayout
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.aurora.extensions.getString
import com.aurora.extensions.isLAndAbove
import com.aurora.extensions.runOnUiThread
import com.aurora.store.R
import com.aurora.store.databinding.ViewActionButtonBinding
import nl.komponents.kovenant.task
import java.util.concurrent.TimeUnit
class ActionButton : RelativeLayout {
@@ -64,23 +70,70 @@ class ActionButton : RelativeLayout {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ActionButton)
val btnTxt = typedArray.getString(R.styleable.ActionButton_btnActionText)
val btnTxtColor = typedArray.getResourceId(
R.styleable.ActionButton_btnActionTextColor,
R.color.colorWhite
)
val stateIcon = typedArray.getResourceId(
R.styleable.ActionButton_btnActionIcon,
R.drawable.ic_check
)
val stateColor = ContextCompat.getColor(context, btnTxtColor)
B.btn.text = btnTxt
B.btn.setTextColor(stateColor)
B.img.setImageDrawable(ContextCompat.getDrawable(context, stateIcon))
if (isLAndAbove()) {
B.img.imageTintList = ColorStateList.valueOf(stateColor)
}
typedArray.recycle()
}
fun setText(text: String) {
B.viewFlipper.displayedChild = 0
B.btn.text = text
}
fun updateProgress(isVisible: Boolean) {
if (isVisible)
B.progress.visibility = View.VISIBLE
else
fun setText(text: Int) {
B.viewFlipper.displayedChild = 0
B.btn.text = getString(text)
}
B.progress.visibility = View.INVISIBLE
fun updateState(state: State) {
val displayChild = when (state) {
State.IDLE -> 0
State.PROGRESS -> 1
State.COMPLETE -> 2
}
if (B.viewFlipper.displayedChild != displayChild) {
runOnUiThread {
B.viewFlipper.displayedChild = displayChild
if (displayChild == 2)
switchToIdle()
}
}
}
private fun switchToIdle() {
task {
TimeUnit.SECONDS.sleep(3)
} success {
updateState(State.IDLE)
}
}
fun addOnClickListener(onClickListener: OnClickListener) {
B.btn.setOnClickListener(onClickListener)
}
enum class State {
IDLE,
PROGRESS,
COMPLETE,
}
}

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.view.custom
package com.aurora.store.view.custom.layouts.button
import android.content.Context
import android.os.Build

View File

@@ -0,0 +1,398 @@
package com.aurora.store.view.custom.progress;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AnimationUtils;
import com.aurora.store.R;
import com.aurora.store.view.custom.progress.indicators.BallPulseIndicator;
import com.aurora.store.view.custom.progress.indicators.Indicator;
public class AuroraProgressView extends View {
private static final String TAG = "AuroraProgressView";
private static final BallPulseIndicator DEFAULT_INDICATOR = new BallPulseIndicator();
private static final int MIN_SHOW_TIME = 500; // ms
private static final int MIN_DELAY = 500; // ms
private long startTime = -1;
private boolean postedHide = false;
private boolean postedShow = false;
private boolean dismissed = false;
private final Runnable mDelayedHide = new Runnable() {
@Override
public void run() {
postedHide = false;
startTime = -1;
setVisibility(View.GONE);
}
};
private final Runnable mDelayedShow = new Runnable() {
@Override
public void run() {
postedShow = false;
if (!dismissed) {
startTime = System.currentTimeMillis();
setVisibility(View.VISIBLE);
}
}
};
int minWidth;
int maxWidth;
int minHeight;
int maxHeight;
private Indicator indicator;
private int indicatorColor;
private boolean mShouldStartAnimationDrawable;
public AuroraProgressView(Context context) {
super(context);
init(context, null, 0, 0);
}
public AuroraProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0, R.style.AuroraProgressView);
}
public AuroraProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, R.style.AuroraProgressView);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public AuroraProgressView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, R.style.AuroraProgressView);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
minWidth = 24;
maxWidth = 48;
minHeight = 24;
maxHeight = 48;
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.AuroraProgressView, defStyleAttr, defStyleRes);
minWidth = a.getDimensionPixelSize(R.styleable.AuroraProgressView_minWidth, minWidth);
maxWidth = a.getDimensionPixelSize(R.styleable.AuroraProgressView_maxWidth, maxWidth);
minHeight = a.getDimensionPixelSize(R.styleable.AuroraProgressView_minHeight, minHeight);
maxHeight = a.getDimensionPixelSize(R.styleable.AuroraProgressView_maxHeight, maxHeight);
String indicatorName = a.getString(R.styleable.AuroraProgressView_indicatorName);
indicatorColor = a.getColor(R.styleable.AuroraProgressView_indicatorColor, Color.WHITE);
setIndicator(indicatorName);
if (indicator == null) {
setIndicator(DEFAULT_INDICATOR);
}
a.recycle();
}
public Indicator getIndicator() {
return indicator;
}
public void setIndicator(Indicator d) {
if (indicator != d) {
if (indicator != null) {
indicator.setCallback(null);
unscheduleDrawable(indicator);
}
indicator = d;
//need to set indicator color again if you didn't specified when you update the indicator .
setIndicatorColor(indicatorColor);
if (d != null) {
d.setCallback(this);
}
postInvalidate();
}
}
public void setIndicatorColor(int color) {
this.indicatorColor = color;
indicator.setColor(color);
}
public void setIndicator(String indicatorName) {
if (TextUtils.isEmpty(indicatorName)) {
return;
}
StringBuilder drawableClassName = new StringBuilder();
if (!indicatorName.contains(".")) {
String defaultPackageName = getClass().getPackage().getName();
drawableClassName.append(defaultPackageName)
.append(".indicators")
.append(".");
}
drawableClassName.append(indicatorName);
try {
Class<?> drawableClass = Class.forName(drawableClassName.toString());
Indicator indicator = (Indicator) drawableClass.newInstance();
setIndicator(indicator);
} catch (ClassNotFoundException e) {
Log.e(TAG, "Didn't find your class , check the name again !");
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void smoothToShow() {
startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_in));
setVisibility(VISIBLE);
}
public void smoothToHide() {
startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_out));
setVisibility(GONE);
}
public void hide() {
dismissed = true;
removeCallbacks(mDelayedShow);
long diff = System.currentTimeMillis() - startTime;
if (diff >= MIN_SHOW_TIME || startTime == -1) {
// The progress spinner has been shown long enough
// OR was not shown yet. If it wasn't shown yet,
// it will just never be shown.
setVisibility(View.GONE);
} else {
// The progress spinner is shown, but not long enough,
// so put a delayed message in to hide it when its been
// shown long enough.
if (!postedHide) {
postDelayed(mDelayedHide, MIN_SHOW_TIME - diff);
postedHide = true;
}
}
}
public void show() {
// Reset the start time.
startTime = -1;
dismissed = false;
removeCallbacks(mDelayedHide);
if (!postedShow) {
postDelayed(mDelayedShow, MIN_DELAY);
postedShow = true;
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return who == indicator
|| super.verifyDrawable(who);
}
void startAnimation() {
if (getVisibility() != VISIBLE) {
return;
}
if (indicator != null) {
mShouldStartAnimationDrawable = true;
}
postInvalidate();
}
void stopAnimation() {
if (indicator != null) {
indicator.stop();
mShouldStartAnimationDrawable = false;
}
postInvalidate();
}
@Override
public void setVisibility(int v) {
if (getVisibility() != v) {
super.setVisibility(v);
if (v == GONE || v == INVISIBLE) {
stopAnimation();
} else {
startAnimation();
}
}
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == GONE || visibility == INVISIBLE) {
stopAnimation();
} else {
startAnimation();
}
}
@Override
public void invalidateDrawable(Drawable dr) {
if (verifyDrawable(dr)) {
final Rect dirty = dr.getBounds();
final int scrollX = getScrollX() + getPaddingLeft();
final int scrollY = getScrollY() + getPaddingTop();
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
} else {
super.invalidateDrawable(dr);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
updateDrawableBounds(w, h);
}
private void updateDrawableBounds(int w, int h) {
// onDraw will translate the canvas so we draw starting at 0,0.
// Subtract out padding for the purposes of the calculations below.
w -= getPaddingRight() + getPaddingLeft();
h -= getPaddingTop() + getPaddingBottom();
int right = w;
int bottom = h;
int top = 0;
int left = 0;
if (indicator != null) {
// Maintain aspect ratio. Certain kinds of animated drawables
// get very confused otherwise.
final int intrinsicWidth = indicator.getIntrinsicWidth();
final int intrinsicHeight = indicator.getIntrinsicHeight();
final float intrinsicAspect = (float) intrinsicWidth / intrinsicHeight;
final float boundAspect = (float) w / h;
if (intrinsicAspect != boundAspect) {
if (boundAspect > intrinsicAspect) {
// New width is larger. Make it smaller to match height.
final int width = (int) (h * intrinsicAspect);
left = (w - width) / 2;
right = left + width;
} else {
// New height is larger. Make it smaller to match width.
final int height = (int) (w * (1 / intrinsicAspect));
top = (h - height) / 2;
bottom = top + height;
}
}
indicator.setBounds(left, top, right, bottom);
}
}
@Override
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawTrack(canvas);
}
void drawTrack(Canvas canvas) {
final Drawable d = indicator;
if (d != null) {
// Translate canvas so a indeterminate circular progress bar with padding
// rotates properly in its animation
final int saveCount = canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
d.draw(canvas);
canvas.restoreToCount(saveCount);
if (mShouldStartAnimationDrawable && d instanceof Animatable) {
((Animatable) d).start();
mShouldStartAnimationDrawable = false;
}
}
}
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int dw = 0;
int dh = 0;
final Drawable d = indicator;
if (d != null) {
dw = Math.max(minWidth, Math.min(maxWidth, d.getIntrinsicWidth()));
dh = Math.max(minHeight, Math.min(maxHeight, d.getIntrinsicHeight()));
}
updateDrawableState();
dw += getPaddingLeft() + getPaddingRight();
dh += getPaddingTop() + getPaddingBottom();
final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0);
final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0);
setMeasuredDimension(measuredWidth, measuredHeight);
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
updateDrawableState();
}
private void updateDrawableState() {
final int[] state = getDrawableState();
if (indicator != null && indicator.isStateful()) {
indicator.setState(state);
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void drawableHotspotChanged(float x, float y) {
super.drawableHotspotChanged(x, y);
if (indicator != null) {
indicator.setHotspot(x, y);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startAnimation();
removeCallbacks();
}
@Override
protected void onDetachedFromWindow() {
stopAnimation();
// This should come after stopAnimation(), otherwise an invalidate message remains in the
// queue, which can prevent the entire view hierarchy from being GC'ed during a rotation
super.onDetachedFromWindow();
removeCallbacks();
}
private void removeCallbacks() {
removeCallbacks(mDelayedHide);
removeCallbacks(mDelayedShow);
}
}

View File

@@ -0,0 +1,51 @@
package com.aurora.store.view.custom.progress.indicators
import android.animation.ValueAnimator
import android.graphics.Canvas
import android.graphics.Paint
import java.util.*
class BallPulseIndicator : Indicator() {
private val scaleFloats = floatArrayOf(
0.75f,
1.0f,
0.75f
)
override fun draw(canvas: Canvas, paint: Paint) {
val circleSpacing = 4f
val radius = (width.coerceAtMost(height) - circleSpacing * 2) / 6
val x = width / 2f - (radius * 2 + circleSpacing)
val y = height / 2f
for (i in 0..2) {
canvas.save()
val translateX = x + radius * 2 * i + circleSpacing * i
canvas.translate(translateX, y)
canvas.scale(scaleFloats[i], scaleFloats[i])
canvas.drawCircle(0f, 0f, radius, paint)
canvas.restore()
}
}
override fun onCreateAnimators(): ArrayList<ValueAnimator> {
val animators = ArrayList<ValueAnimator>()
val delays = intArrayOf(120, 240, 360)
for (i in 0..2) {
val scaleAnim = ValueAnimator.ofFloat(1f, 0.3f, 1f)
scaleAnim.duration = 1000
scaleAnim.repeatCount = -1
scaleAnim.startDelay = delays[i].toLong()
addUpdateListener(scaleAnim) {
scaleFloats[i] = it.animatedValue as Float
postInvalidate()
}
animators.add(scaleAnim)
}
return animators
}
}

View File

@@ -0,0 +1,191 @@
package com.aurora.store.view.custom.progress.indicators;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import java.util.ArrayList;
import java.util.HashMap;
public abstract class Indicator extends Drawable implements Animatable {
private static final Rect ZERO_BOUNDS_RECT = new Rect();
private final HashMap<ValueAnimator, ValueAnimator.AnimatorUpdateListener> updateListenerHashMap = new HashMap<>();
private final Paint paint = new Paint();
private int alpha = 255;
private ArrayList<ValueAnimator> animators;
private boolean hasAnimators;
protected Rect drawBounds = ZERO_BOUNDS_RECT;
public Indicator() {
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
}
public int getColor() {
return paint.getColor();
}
public void setColor(int color) {
paint.setColor(color);
}
@Override
public void setAlpha(int alpha) {
this.alpha = alpha;
}
@Override
public int getAlpha() {
return alpha;
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
}
@Override
public void draw(Canvas canvas) {
draw(canvas, paint);
}
public abstract void draw(Canvas canvas, Paint paint);
public abstract ArrayList<ValueAnimator> onCreateAnimators();
@Override
public void start() {
ensureAnimators();
if (animators == null) {
return;
}
// If the animators has not ended, do nothing.
if (isStarted()) {
return;
}
startAnimators();
invalidateSelf();
}
private void startAnimators() {
for (int i = 0; i < animators.size(); i++) {
ValueAnimator animator = animators.get(i);
//when the animator restart , add the updateListener again because they
// was removed by animator stop .
ValueAnimator.AnimatorUpdateListener updateListener = updateListenerHashMap.get(animator);
if (updateListener != null) {
animator.addUpdateListener(updateListener);
}
animator.start();
}
}
private void stopAnimators() {
if (animators != null) {
for (ValueAnimator animator : animators) {
if (animator != null && animator.isStarted()) {
animator.removeAllUpdateListeners();
animator.end();
}
}
}
}
private void ensureAnimators() {
if (!hasAnimators) {
animators = onCreateAnimators();
hasAnimators = true;
}
}
@Override
public void stop() {
stopAnimators();
}
private boolean isStarted() {
for (ValueAnimator animator : animators) {
return animator.isStarted();
}
return false;
}
@Override
public boolean isRunning() {
for (ValueAnimator animator : animators) {
return animator.isRunning();
}
return false;
}
public void addUpdateListener(ValueAnimator animator, ValueAnimator.AnimatorUpdateListener updateListener) {
updateListenerHashMap.put(animator, updateListener);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
setDrawBounds(bounds);
}
public void setDrawBounds(Rect drawBounds) {
setDrawBounds(drawBounds.left, drawBounds.top, drawBounds.right, drawBounds.bottom);
}
public void setDrawBounds(int left, int top, int right, int bottom) {
this.drawBounds = new Rect(left, top, right, bottom);
}
public void postInvalidate() {
invalidateSelf();
}
public Rect getDrawBounds() {
return drawBounds;
}
public int getWidth() {
return drawBounds.width();
}
public int getHeight() {
return drawBounds.height();
}
public int centerX() {
return drawBounds.centerX();
}
public int centerY() {
return drawBounds.centerY();
}
public float exactCenterX() {
return drawBounds.exactCenterX();
}
public float exactCenterY() {
return drawBounds.exactCenterY();
}
}

View File

@@ -0,0 +1,70 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.controller
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.store.view.epoxy.groups.CarouselModelGroup
import com.aurora.store.view.epoxy.groups.CarouselShimmerGroup
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
open class DetailsCarouselController(private val callbacks: Callbacks) :
GenericCarouselController(callbacks) {
override fun buildModels(streamBundle: StreamBundle?) {
setFilterDuplicates(true)
if (streamBundle == null) {
for (i in 1..2) {
add(
CarouselShimmerGroup()
.id(i)
)
}
} else {
if (streamBundle.streamClusters.isNotEmpty()) {
if (streamBundle.streamClusters.size == 1) {
streamBundle
.streamClusters
.values
.filter { applyFilter(it) }
.forEach { streamCluster ->
streamCluster.clusterAppList.forEach {
add(
AppListViewModel_()
.id(it.id)
.app(it)
.click { _ -> callbacks.onAppClick(it) }
)
}
}
} else {
streamBundle
.streamClusters
.values
.filter { applyFilter(it) }
.forEach { streamCluster ->
add(CarouselModelGroup(streamCluster, callbacks))
}
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.controller
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.view.epoxy.groups.CarouselShimmerGroup
import com.aurora.store.view.epoxy.groups.DeveloperModelGroup
open class DeveloperCarouselController(private val callbacks: Callbacks) :
GenericCarouselController(callbacks) {
override fun applyFilter(streamBundle: StreamCluster): Boolean {
return streamBundle.clusterTitle.isNotBlank() //Filter noisy cluster
&& streamBundle.clusterAppList.isNotEmpty() //Filter empty clusters
}
override fun buildModels(streamBundle: StreamBundle?) {
setFilterDuplicates(true)
if (streamBundle == null) {
for (i in 1..2) {
add(
CarouselShimmerGroup()
.id(i)
)
}
} else {
streamBundle
.streamClusters
.values
.filter { applyFilter(it) }
.forEach { streamCluster ->
add(DeveloperModelGroup(streamCluster, callbacks))
}
}
}
}

View File

@@ -23,7 +23,7 @@ import com.airbnb.epoxy.TypedEpoxyController
import com.aurora.gplayapi.data.models.editor.EditorChoiceBundle
import com.aurora.gplayapi.data.models.editor.EditorChoiceCluster
import com.aurora.store.view.epoxy.groups.EditorChoiceModelGroup
import com.aurora.store.view.epoxy.views.EditorHeadViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
class EditorChoiceController(private val callbacks: Callbacks) :
TypedEpoxyController<List<EditorChoiceBundle>>() {
@@ -37,7 +37,7 @@ class EditorChoiceController(private val callbacks: Callbacks) :
val idPrefix = editorChoiceBundle.id
add(
EditorHeadViewModel_()
HeaderViewModel_()
.id("header_${idPrefix}")
.title(editorChoiceBundle.bundleTitle)
)

View File

@@ -26,7 +26,7 @@ import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.view.epoxy.groups.CarouselModelGroup
import com.aurora.store.view.epoxy.groups.CarouselShimmerGroup
import com.aurora.store.view.epoxy.views.AppListViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.app.NoAppViewModel_

View File

@@ -26,7 +26,7 @@ import com.aurora.store.R
import com.aurora.store.util.Log
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.AppViewModel_
import com.aurora.store.view.epoxy.views.app.AppViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
class CarouselModelGroup(

View File

@@ -0,0 +1,127 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.groups
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyModelGroup
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.util.Log
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.app.AppViewModel_
import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_
class DeveloperModelGroup(
streamCluster: StreamCluster,
callbacks: GenericCarouselController.Callbacks
) :
EpoxyModelGroup(
R.layout.model_developer_carousel_group, buildModels(
streamCluster,
callbacks
)
) {
companion object {
private fun buildModels(
streamCluster: StreamCluster,
callbacks: GenericCarouselController.Callbacks
): List<EpoxyModel<*>> {
val models = ArrayList<EpoxyModel<*>>()
val clusterViewModels = mutableListOf<EpoxyModel<*>>()
val screenshotsViewModels = mutableListOf<EpoxyModel<*>>()
val idPrefix = streamCluster.id
models.add(
HeaderViewModel_()
.id("${idPrefix}_header")
.title(streamCluster.clusterTitle)
.browseUrl(streamCluster.clusterBrowseUrl)
.click { _ ->
callbacks.onHeaderClicked(streamCluster)
}
)
if (streamCluster.clusterAppList.size == 1) {
val app = streamCluster.clusterAppList[0]
for (artwork in app.screenshots) {
screenshotsViewModels.add(
ScreenshotViewModel_()
.id(artwork.url)
.artwork(artwork)
)
}
clusterViewModels.add(
AppListViewModel_()
.id(app.id)
.app(app)
.click { _ ->
callbacks.onAppClick(app)
}
)
} else {
for (app in streamCluster.clusterAppList) {
clusterViewModels.add(
AppViewModel_()
.id(app.id)
.app(app)
.click { _ ->
callbacks.onAppClick(app)
}
.longClick { _ ->
callbacks.onAppLongClick(app)
false
}
.onBind { _, _, position ->
val itemCount = clusterViewModels.count()
if (itemCount >= 2) {
if (position == clusterViewModels.count() - 2) {
callbacks.onClusterScrolled(streamCluster)
Log.i("Cluster %s Scrolled", streamCluster.clusterTitle)
}
}
}
)
}
}
if (screenshotsViewModels.isNotEmpty()) {
models.add(
CarouselHorizontalModel_()
.id("${idPrefix}_screenshots")
.models(screenshotsViewModels)
)
}
models.add(
CarouselHorizontalModel_()
.id("${idPrefix}_cluster")
.models(clusterViewModels)
)
return models
}
}
}

View File

@@ -24,19 +24,16 @@ import com.airbnb.epoxy.EpoxyModelGroup
import com.aurora.gplayapi.data.models.editor.EditorChoiceCluster
import com.aurora.store.R
import com.aurora.store.view.epoxy.controller.EditorChoiceController
import com.aurora.store.view.epoxy.views.EditorHeadViewModel_
import com.aurora.store.view.epoxy.views.EditorImageViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
class EditorChoiceModelGroup(
editorChoiceCluster: EditorChoiceCluster,
callbacks: EditorChoiceController.Callbacks
) :
EpoxyModelGroup(
R.layout.model_editorchoice_group, buildModels(
editorChoiceCluster,
callbacks
)
) {
) : EpoxyModelGroup(
R.layout.model_editorchoice_group,
buildModels(editorChoiceCluster, callbacks)
) {
companion object {
private fun buildModels(
editorChoiceCluster: EditorChoiceCluster,
@@ -48,21 +45,33 @@ class EditorChoiceModelGroup(
val idPrefix = editorChoiceCluster.id
models.add(
HeaderViewModel_()
EditorImageViewModel_()
.id("artwork_header_${idPrefix}")
.artwork(editorChoiceCluster.clusterArtwork[0])
.click { _ -> callbacks.onClick(editorChoiceCluster) }
)
models.add(
EditorHeadViewModel_()
.id("header_${idPrefix}")
.title(editorChoiceCluster.clusterTitle)
.browseUrl(editorChoiceCluster.clusterBrowseUrl)
.click { _ -> callbacks.onClick(editorChoiceCluster) }
)
editorChoiceCluster.clusterArtwork.forEach {
clusterViewModels.add(
EditorImageViewModel_()
.id("artwork_${idPrefix}")
.artwork(it)
)
}
editorChoiceCluster.clusterArtwork
.drop(1)
.forEach {
clusterViewModels.add(
EditorImageViewModel_()
.id("artwork_${idPrefix}_${it.url}")
.artwork(it)
.click { _ -> callbacks.onClick(editorChoiceCluster) }
)
}
models.add(
CarouselHorizontalModel_()

View File

@@ -22,6 +22,7 @@ package com.aurora.store.view.epoxy.views
import android.view.View
import android.view.animation.AnimationUtils
import com.airbnb.epoxy.EpoxyModel
import com.aurora.store.view.epoxy.views.app.AppListView
abstract class BaseView<T : View?> : EpoxyModel<T>() {

View File

@@ -30,15 +30,15 @@ import com.airbnb.epoxy.OnViewRecycled
import com.aurora.store.R
import com.aurora.store.data.model.Black
import com.aurora.store.databinding.ViewBlackBinding
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.load
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class BlackView : RelativeLayout {
class BlackListView : RelativeLayout {
private lateinit var B: ViewBlackBinding

View File

@@ -29,12 +29,12 @@ import com.airbnb.epoxy.OnViewRecycled
import com.aurora.gplayapi.data.models.Category
import com.aurora.store.R
import com.aurora.store.databinding.ViewCategoryBinding
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.load
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class CategoryView : RelativeLayout {

View File

@@ -34,8 +34,8 @@ import com.aurora.store.databinding.ViewDownloadBinding
import com.aurora.store.util.CommonUtil.getDownloadSpeedString
import com.aurora.store.util.CommonUtil.getETAString
import com.aurora.store.util.CommonUtil.humanReadableByteValue
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.load
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.google.gson.Gson
@@ -45,7 +45,7 @@ import java.lang.reflect.Modifier
import java.util.*
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class DownloadView : RelativeLayout {

View File

@@ -22,13 +22,15 @@ package com.aurora.store.view.epoxy.views
import android.content.Context
import android.util.AttributeSet
import android.widget.RelativeLayout
import androidx.core.text.HtmlCompat
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.store.R
import com.aurora.store.databinding.ViewEditorHeadBinding
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class EditorHeadView : RelativeLayout {
@@ -56,6 +58,11 @@ class EditorHeadView : RelativeLayout {
B = ViewEditorHeadBinding.bind(view)
}
@CallbackProp
fun click(onClickListener: OnClickListener?) {
B.root.setOnClickListener(onClickListener)
}
@ModelProp
fun title(title: String) {
B.title.text = title

View File

@@ -22,14 +22,16 @@ package com.aurora.store.view.epoxy.views
import android.content.Context
import android.util.AttributeSet
import android.widget.RelativeLayout
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.ViewEditorImageBinding
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.load
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.aurora.extensions.px
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
@@ -66,23 +68,30 @@ class EditorImageView : RelativeLayout {
fun artwork(artwork: Artwork) {
when (artwork.type) {
14 -> {
B.img.layoutParams.width =
resources.getDimension(R.dimen.icon_size_large).toInt() * 2
B.img.layoutParams.height = 108.px.toInt()
B.img.layoutParams.width = 192.px.toInt()
B.img.requestLayout()
B.img.load(artwork.url, DrawableTransitionOptions.withCrossFade()) {
transform(RoundedCorners(16))
transform(RoundedCorners(8.px.toInt()))
}
}
else -> {
B.img.layoutParams.width = resources.getDimension(R.dimen.icon_size_large).toInt()
B.img.layoutParams.width = 24.px.toInt()
B.img.layoutParams.height = 24.px.toInt()
B.img.requestLayout()
B.img.load(artwork.url, DrawableTransitionOptions.withCrossFade()) {
transform(RoundedCorners(16))
transform(RoundedCorners(4.px.toInt()))
}
}
}
}
@CallbackProp
fun click(onClickListener: OnClickListener?) {
B.root.setOnClickListener(onClickListener)
}
@OnViewRecycled
fun clear() {
B.img.clear()

View File

@@ -32,7 +32,7 @@ import com.aurora.store.databinding.ViewHeaderBinding
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class HeaderView : RelativeLayout {

View File

@@ -0,0 +1,57 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views
import android.content.Context
import android.util.AttributeSet
import android.widget.RelativeLayout
import com.airbnb.epoxy.ModelView
import com.aurora.store.R
import com.aurora.store.databinding.ViewDividerBinding
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class HorizontalDividerView : RelativeLayout {
private lateinit var B: ViewDividerBinding
constructor(context: Context?) : super(context) {
init(context, null)
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}
private fun init(context: Context?, attrs: AttributeSet?) {
val view = inflate(context, R.layout.view_divider, this)
B = ViewDividerBinding.bind(view)
}
}

View File

@@ -31,7 +31,7 @@ import com.aurora.store.databinding.ViewActionHeaderBinding
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class MinimalHeaderView : RelativeLayout {

View File

@@ -30,8 +30,8 @@ import com.airbnb.epoxy.OnViewRecycled
import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.store.R
import com.aurora.store.databinding.ViewSearchSuggestionBinding
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.load
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@ModelView(

View File

@@ -31,7 +31,7 @@ import com.aurora.store.databinding.ViewHeaderUpdateBinding
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class UpdateHeaderView : RelativeLayout {

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.view.epoxy.views
package com.aurora.store.view.epoxy.views.app
import android.content.Context
import android.util.AttributeSet
@@ -30,9 +30,10 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.databinding.ViewAppListBinding
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.getString
import com.aurora.store.util.extensions.load
import com.aurora.extensions.clear
import com.aurora.extensions.getString
import com.aurora.extensions.load
import com.aurora.store.view.epoxy.views.BaseView
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@ModelView(
@@ -90,7 +91,7 @@ class AppListView : RelativeLayout {
if (app.dependencies.dependentPackages.isNotEmpty())
extras.add(getString(R.string.details_gsf_dependent))
B.txtLine3.text = extras.joinToString(separator = " ")
B.txtLine3.text = extras.joinToString(separator = " ")
}
@CallbackProp

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.view.epoxy.views
package com.aurora.store.view.epoxy.views.app
import android.content.Context
import android.util.AttributeSet
@@ -28,17 +28,18 @@ import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.databinding.ViewAppUpdateBinding
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.load
import com.aurora.store.view.epoxy.views.BaseView
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class AppUpdateView : RelativeLayout {
@@ -76,7 +77,8 @@ class AppUpdateView : RelativeLayout {
}
B.txtLine2.text = app.developerName
B.txtLine3.text = CommonUtil.addSiPrefix(app.size)
B.txtLine3.text = ("${CommonUtil.addSiPrefix(app.size)}${app.updatedOn}")
B.txtLine4.text = ("v${app.versionName}.${app.versionCode}")
B.txtChangelog.text = if (app.changes.isNotEmpty())
HtmlCompat.fromHtml(
app.changes,

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.view.epoxy.views
package com.aurora.store.view.epoxy.views.app
import android.content.Context
import android.util.AttributeSet
@@ -30,8 +30,9 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.databinding.ViewAppBinding
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.load
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.aurora.store.view.epoxy.views.BaseView
import com.bumptech.glide.load.resource.bitmap.RoundedCorners

View File

@@ -29,8 +29,8 @@ import com.airbnb.epoxy.OnViewRecycled
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.databinding.ViewAppDependentBinding
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.load
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.aurora.store.view.epoxy.views.BaseView
import com.bumptech.glide.load.resource.bitmap.RoundedCorners

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.view.epoxy.views
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
@@ -29,8 +29,8 @@ import com.airbnb.epoxy.OnViewRecycled
import com.aurora.gplayapi.data.models.details.Badge
import com.aurora.store.R
import com.aurora.store.databinding.ViewBadgeBinding
import com.aurora.store.util.extensions.load
import com.aurora.extensions.load
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.view.epoxy.views
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
@@ -29,7 +29,7 @@ import com.airbnb.epoxy.OnViewRecycled
import com.aurora.store.R
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.databinding.ViewExodusBinding
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.view.epoxy.views
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
@@ -28,6 +28,7 @@ import com.aurora.gplayapi.data.models.File
import com.aurora.store.R
import com.aurora.store.databinding.ViewFileBinding
import com.aurora.store.util.CommonUtil
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,

View File

@@ -0,0 +1,74 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import android.widget.RelativeLayout
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled
import com.aurora.store.R
import com.aurora.store.databinding.ViewInfoBinding
import com.aurora.store.view.epoxy.views.BaseView
import java.util.*
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class InfoView : RelativeLayout {
private lateinit var B: ViewInfoBinding
constructor(context: Context?) : super(context) {
init(context, null)
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}
private fun init(context: Context?, attrs: AttributeSet?) {
val view = inflate(context, R.layout.view_info, this)
B = ViewInfoBinding.bind(view)
}
@ModelProp
fun badge(info: Map.Entry<String, String>) {
B.txtTitle.text = info.key
.replace("_", " ")
.toLowerCase(Locale.getDefault())
.capitalize(Locale.getDefault())
B.txtSubtitle.text = info.value
}
@OnViewRecycled
fun clear() {
}
}

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.view.epoxy.views
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.content.res.Resources
@@ -31,16 +31,16 @@ import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.GlideApp
import com.aurora.store.R
import com.aurora.store.databinding.ViewScreenshotLargeBinding
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.px
import com.aurora.store.util.extensions.runOnUiThread
import com.aurora.extensions.clear
import com.aurora.extensions.px
import com.aurora.extensions.runOnUiThread
import com.aurora.store.view.epoxy.views.BaseView
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_MATCH_HEIGHT,
baseModelClass = BaseView::class
@@ -72,8 +72,10 @@ class LargeScreenshotView : RelativeLayout {
@ModelProp
fun artwork(artwork: Artwork) {
val displayMetrics = Resources.getSystem().displayMetrics
GlideApp.with(context)
.load(artwork.url)
.load("${artwork.url}=rw-w${displayMetrics.widthPixels}-v1-e15")
.placeholder(R.drawable.bg_placeholder)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.addListener(object : RequestListener<Drawable> {
override fun onLoadFailed(
@@ -97,7 +99,6 @@ class LargeScreenshotView : RelativeLayout {
B.img.layoutParams.height = artwork.height.px.toInt()
B.img.layoutParams.width = artwork.width.px.toInt()
} else {
val displayMetrics = Resources.getSystem().displayMetrics
val height = displayMetrics.heightPixels
val width = displayMetrics.widthPixels
B.img.layoutParams.width = width

View File

@@ -0,0 +1,126 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import android.widget.RelativeLayout
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.ViewScreenshotMiniBinding
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.aurora.extensions.px
import com.aurora.store.view.epoxy.views.BaseView
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class MiniScreenshotView : RelativeLayout {
private lateinit var B: ViewScreenshotMiniBinding
private var position: Int = 0
interface ScreenshotCallback {
fun onClick(position: Int = 0)
}
constructor(context: Context?) : super(context) {
init(context, null)
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}
private fun init(context: Context?, attrs: AttributeSet?) {
val view = inflate(context, R.layout.view_screenshot_mini, this)
B = ViewScreenshotMiniBinding.bind(view)
}
@ModelProp
fun position(pos: Int) {
position = pos
}
@ModelProp
fun artwork(artwork: Artwork) {
normalizeSize(artwork)
B.img.load("${artwork.url}=rw-w480-v1-e15", DrawableTransitionOptions.withCrossFade()) {
placeholder(R.drawable.bg_rounded)
transform(RoundedCorners(8.px.toInt()))
}
}
private fun normalizeSize(artwork: Artwork) {
if (artwork.height != 0 && artwork.width != 0) {
val artworkHeight = artwork.height
val artworkWidth = artwork.width
val normalizedHeight: Float
val normalizedWidth: Float
when {
artworkHeight == artworkWidth -> {
normalizedHeight = 120f
normalizedWidth = 120f
}
else -> {
val factor = artworkHeight / 120f
normalizedHeight = 120f
normalizedWidth = (artworkWidth / factor)
}
}
B.img.layoutParams.height = normalizedHeight.px.toInt()
B.img.layoutParams.width = normalizedWidth.px.toInt()
B.img.requestLayout()
}
}
@CallbackProp
fun callback(screenshotCallback: ScreenshotCallback?) {
B.img.setOnClickListener {
screenshotCallback?.onClick(position)
}
}
@OnViewRecycled
fun clear() {
B.img.clear()
}
}

View File

@@ -30,10 +30,9 @@ import com.airbnb.epoxy.OnViewRecycled
import com.aurora.gplayapi.data.models.details.Badge
import com.aurora.store.R
import com.aurora.store.databinding.ViewMoreBadgeBinding
import com.aurora.store.util.extensions.load
import com.aurora.extensions.load
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.view.epoxy.views
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
@@ -29,9 +29,10 @@ import com.airbnb.epoxy.OnViewRecycled
import com.aurora.gplayapi.data.models.Review
import com.aurora.store.R
import com.aurora.store.databinding.ViewReviewBinding
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.load
import com.aurora.store.util.extensions.toDate
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.aurora.extensions.toDate
import com.aurora.store.view.epoxy.views.BaseView
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions

View File

@@ -17,7 +17,7 @@
*
*/
package com.aurora.store.view.epoxy.views
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
@@ -29,13 +29,13 @@ import com.airbnb.epoxy.OnViewRecycled
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.ViewScreenshotBinding
import com.aurora.store.util.extensions.clear
import com.aurora.store.util.extensions.load
import com.aurora.store.util.extensions.px
import com.aurora.extensions.clear
import com.aurora.extensions.load
import com.aurora.extensions.px
import com.aurora.store.view.epoxy.views.BaseView
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
@@ -79,7 +79,7 @@ class ScreenshotView : RelativeLayout {
@ModelProp
fun artwork(artwork: Artwork) {
normalizeSize(artwork)
B.img.load(artwork.url, DrawableTransitionOptions.withCrossFade()) {
B.img.load("${artwork.url}=rw-w480-v1-e15", DrawableTransitionOptions.withCrossFade()) {
placeholder(R.drawable.bg_rounded)
transform(RoundedCorners(8.px.toInt()))
}
@@ -88,22 +88,22 @@ class ScreenshotView : RelativeLayout {
private fun normalizeSize(artwork: Artwork) {
if (artwork.height != 0 && artwork.width != 0) {
val viewHeight = artwork.height
val viewWidth = artwork.width
val artworkHeight = artwork.height
val artworkWidth = artwork.width
var normalizedHeight: Int = viewHeight
var normalizedWidth: Int = viewWidth
val normalizedHeight: Float
val normalizedWidth: Float
if (viewHeight == viewWidth) {
normalizedHeight = 240
normalizedWidth = 240
} else if (viewHeight > viewWidth) {
normalizedHeight = 240
normalizedWidth = 135
} else if (viewHeight < viewWidth) {
normalizedHeight = 240
normalizedWidth = 427
when {
artworkHeight == artworkWidth -> {
normalizedHeight = 192f
normalizedWidth = 192f
}
else -> {
val factor = artworkHeight / 192f
normalizedHeight = 192f
normalizedWidth = (artworkWidth / factor)
}
}
B.img.layoutParams.height = normalizedHeight.px.toInt()

View File

@@ -76,4 +76,9 @@ class InstallerView : RelativeLayout {
fun checked(onCheckedChangeListener: CompoundButton.OnCheckedChangeListener?) {
B.checkbox.setOnCheckedChangeListener(onCheckedChangeListener)
}
@CallbackProp
fun click(onClickListener: OnClickListener?) {
B.root.setOnClickListener(onClickListener)
}
}

View File

@@ -28,9 +28,9 @@ import com.airbnb.epoxy.ModelView
import com.aurora.store.R
import com.aurora.store.data.model.Link
import com.aurora.store.databinding.ViewLinkBinding
import com.aurora.store.util.extensions.hide
import com.aurora.store.util.extensions.load
import com.aurora.store.util.extensions.show
import com.aurora.extensions.hide
import com.aurora.extensions.load
import com.aurora.extensions.show
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(

View File

@@ -26,10 +26,10 @@ import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.data.model.Link
import com.aurora.store.databinding.ActivityAboutBinding
import com.aurora.store.util.extensions.browse
import com.aurora.store.util.extensions.close
import com.aurora.store.util.extensions.copyToClipBoard
import com.aurora.store.util.extensions.load
import com.aurora.extensions.browse
import com.aurora.extensions.close
import com.aurora.extensions.copyToClipBoard
import com.aurora.extensions.load
import com.aurora.store.view.epoxy.views.preference.LinkViewModel_
import com.aurora.store.view.ui.commons.BaseActivity

View File

@@ -19,22 +19,19 @@
package com.aurora.store.view.ui.account
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import com.aurora.extensions.browse
import com.aurora.extensions.close
import com.aurora.extensions.load
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.store.MainActivity
import com.aurora.store.R
import com.aurora.store.data.AuthState
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.providers.AccountProvider
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.databinding.ActivityAccountBinding
import com.aurora.store.util.CommonUtil.getEmptyActivityBundle
import com.aurora.store.util.extensions.browse
import com.aurora.store.util.extensions.close
import com.aurora.store.util.extensions.load
import com.aurora.store.view.ui.commons.BaseActivity
import com.aurora.store.viewmodel.auth.AuthViewModel
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@@ -224,14 +221,6 @@ class AccountActivity : BaseActivity() {
}
}
private fun moveToContent() {
runOnUiThread {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent, getEmptyActivityBundle(this))
}
}
override fun onConnected() {
hideNetworkConnectivitySheet()
}

View File

@@ -32,9 +32,9 @@ import com.aurora.store.data.event.BusEvent
import com.aurora.store.databinding.ActivityGoogleBinding
import com.aurora.store.util.AC2DMTask
import com.aurora.store.util.Preferences
import com.aurora.store.util.Util
import com.aurora.store.util.extensions.close
import com.aurora.store.util.extensions.isLAndAbove
import com.aurora.store.util.AC2DMUtil
import com.aurora.extensions.close
import com.aurora.extensions.isLAndAbove
import com.aurora.store.view.ui.commons.BaseActivity
import nl.komponents.kovenant.task
import org.greenrobot.eventbus.EventBus
@@ -79,7 +79,7 @@ class GoogleActivity : BaseActivity() {
B.webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
val cookies = CookieManager.getInstance().getCookie(url)
val cookieMap = Util.parseCookieString(cookies)
val cookieMap = AC2DMUtil.parseCookieString(cookies)
if (cookieMap.isNotEmpty() && cookieMap[AUTH_TOKEN] != null) {
val oauthToken = cookieMap[AUTH_TOKEN]
B.webview.evaluateJavascript("(function() { return document.getElementById('profileIdentifier').innerHTML; })();") {

View File

@@ -28,7 +28,7 @@ import com.aurora.gplayapi.data.models.AuthData
import com.aurora.store.R
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.databinding.ActivityGenericPagerBinding
import com.aurora.store.util.extensions.close
import com.aurora.extensions.close
import com.aurora.store.view.ui.commons.BaseActivity
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@@ -73,6 +73,7 @@ class AppsGamesActivity : BaseActivity() {
when (position) {
0 -> tab.text = getString(R.string.title_installed)
1 -> tab.text = getString(R.string.title_library)
2 -> tab.text = getString(R.string.title_purchase_history)
else -> {
}
}
@@ -89,6 +90,7 @@ class AppsGamesActivity : BaseActivity() {
return when (position) {
0 -> InstalledAppsFragment.newInstance()
1 -> LibraryAppsFragment.newInstance()
2 -> PurchasedAppsFragment.newInstance()
else -> Fragment()
}
}
@@ -97,7 +99,7 @@ class AppsGamesActivity : BaseActivity() {
return if (isAnonymous)
1
else
2
3
}
}
}

View File

@@ -28,7 +28,7 @@ import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.databinding.FragmentUpdatesBinding
import com.aurora.store.view.epoxy.views.AppListViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.view.ui.commons.BaseFragment

View File

@@ -28,7 +28,7 @@ import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.databinding.FragmentUpdatesBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.views.AppListViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_

View File

@@ -0,0 +1,145 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.all
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.databinding.FragmentUpdatesBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.view.ui.sheets.AppMenuSheet
import com.aurora.store.viewmodel.all.InstalledViewModel
import com.aurora.store.viewmodel.all.PurchasedViewModel
class PurchasedAppsFragment : BaseFragment() {
private lateinit var VM: PurchasedViewModel
private lateinit var B: FragmentUpdatesBinding
lateinit var endlessRecyclerOnScrollListener: EndlessRecyclerOnScrollListener
companion object {
@JvmStatic
fun newInstance(): PurchasedAppsFragment {
return PurchasedAppsFragment().apply {
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
B = FragmentUpdatesBinding.bind(
inflater.inflate(
R.layout.fragment_updates,
container,
false
)
)
VM = ViewModelProvider(requireActivity()).get(PurchasedViewModel::class.java)
return B.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
VM.liveData.observe(viewLifecycleOwner, {
updateController(it)
B.swipeRefreshLayout.isRefreshing = false
})
B.swipeRefreshLayout.setOnRefreshListener {
VM.observe()
}
attachRecycler()
updateController(null)
}
private fun attachRecycler() {
endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener() {
override fun onLoadMore(currentPage: Int) {
VM.observe()
}
}
B.recycler.addOnScrollListener(endlessRecyclerOnScrollListener)
}
private fun updateController(appList: List<App>?) {
B.recycler.withModels {
setFilterDuplicates(true)
if (appList == null) {
for (i in 1..6) {
add(
AppListViewShimmerModel_()
.id(i)
)
}
} else {
add(
HeaderViewModel_()
.id("header")
.title("${appList.size} apps purchased")
)
appList.forEach { app ->
add(
AppListViewModel_()
.id(app.id)
.app(app)
.click { _ -> openDetailsActivity(app) }
.longClick { _ ->
openAppMenuSheet(app)
false
}
)
}
}
}
}
private fun openAppMenuSheet(app: App) {
val fragment = childFragmentManager.findFragmentByTag(AppMenuSheet.TAG)
if (fragment != null)
childFragmentManager.beginTransaction().remove(fragment)
AppMenuSheet().apply {
arguments = Bundle().apply {
putString(Constants.STRING_EXTRA, gson.toJson(app))
}
}.show(
childFragmentManager,
AppMenuSheet.TAG
)
}
}

View File

@@ -70,10 +70,10 @@ class AppsContainerFragment : Fragment() {
TabLayoutMediator(B.tabLayout, B.pager, true) { tab: TabLayout.Tab, position: Int ->
when (position) {
0 -> tab.text = "For you"
1 -> tab.text = "Top charts"
2 -> tab.text = "Categories"
3 -> tab.text = "Editor's choice"
0 -> tab.text = getString(R.string.tab_for_you)
1 -> tab.text = getString(R.string.tab_top_charts)
2 -> tab.text = getString(R.string.tab_categories)
3 -> tab.text = getString(R.string.tab_editor_choice)
else -> {
}
}

View File

@@ -29,8 +29,8 @@ import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.databinding.FragmentTopContainerBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.views.AppListViewModel_
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.topchart.*
@@ -40,6 +40,8 @@ class TopChartFragment : BaseFragment() {
private lateinit var VM: BaseChartViewModel
private lateinit var B: FragmentTopContainerBinding
private lateinit var streamCluster: StreamCluster
private var chartType = 0
private var chartCategory = 0
@@ -88,16 +90,23 @@ class TopChartFragment : BaseFragment() {
super.onViewCreated(view, savedInstanceState)
VM.liveData.observe(viewLifecycleOwner, {
updateController(it)
if (!::streamCluster.isInitialized)
attachRecycler()
streamCluster = it
updateController(streamCluster)
})
updateController(null)
}
private fun attachRecycler() {
B.recycler.addOnScrollListener(object : EndlessRecyclerOnScrollListener() {
override fun onLoadMore(currentPage: Int) {
VM.nextCluster()
}
})
updateController(null)
}
private fun updateController(streamCluster: StreamCluster?) {

View File

@@ -24,19 +24,16 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import com.aurora.Constants
import com.aurora.extensions.applyTheme
import com.aurora.extensions.getEmptyActivityBundle
import com.aurora.gplayapi.data.models.App
import com.aurora.store.data.providers.NetworkProvider
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_THEME_ACCENT
import com.aurora.store.util.Preferences.PREFERENCE_THEME_TYPE
import com.aurora.store.util.ViewUtil
import com.aurora.store.util.extensions.applyTheme
import com.aurora.store.view.ui.account.GoogleActivity
import com.aurora.store.view.ui.details.AppDetailsActivity
import com.aurora.store.view.ui.details.DetailsMoreActivity
import com.aurora.store.view.ui.details.DetailsReviewActivity
import com.aurora.store.view.ui.details.*
import com.aurora.store.view.ui.sheets.NetworkDialogSheet
import com.google.gson.Gson
import com.google.gson.GsonBuilder
@@ -99,42 +96,66 @@ abstract class BaseActivity : AppCompatActivity(), NetworkProvider.NetworkListen
}
}
fun openStreamBrowseActivity(browseUrl: String) {
val intent = Intent(this, StreamBrowseActivity::class.java)
fun openStreamBrowseActivity(browseUrl: String, title: String = "") {
val intent = if (browseUrl.toLowerCase().contains("expanded"))
Intent(this, ExpandedStreamBrowseActivity::class.java)
else if (browseUrl.toLowerCase().contains("developer"))
Intent(this, DevProfileActivity::class.java)
else
Intent(this, StreamBrowseActivity::class.java)
intent.putExtra(Constants.BROWSE_EXTRA, browseUrl)
intent.putExtra(Constants.STRING_EXTRA, title)
startActivity(
intent,
ViewUtil.getEmptyActivityBundle(this)
getEmptyActivityBundle()
)
}
fun openScreenshotActivity(app: App, position: Int) {
val intent = Intent(
this,
ScreenshotActivity::class.java
).apply {
putExtra(Constants.STRING_EXTRA, gson.toJson(app.screenshots))
putExtra(Constants.INT_EXTRA, position)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val options =
ActivityOptions.makeSceneTransitionAnimation(this)
startActivity(intent, options.toBundle())
} else {
startActivity(intent)
}
}
fun openGoogleActivity() {
val intent = Intent(this, GoogleActivity::class.java)
startActivity(
intent,
ViewUtil.getEmptyActivityBundle(this)
getEmptyActivityBundle()
)
}
fun showNetworkConnectivitySheet() {
supportFragmentManager.beginTransaction()
.add(NetworkDialogSheet.newInstance(0), "NDS")
.commitAllowingStateLoss()
runOnUiThread {
supportFragmentManager.beginTransaction()
.add(NetworkDialogSheet.newInstance(), NetworkDialogSheet.TAG)
.commitAllowingStateLoss()
}
}
fun hideNetworkConnectivitySheet() {
val fragment: Fragment? = supportFragmentManager.findFragmentByTag("NDS")
if (fragment != null) {
supportFragmentManager.beginTransaction()
.remove(fragment)
.commitAllowingStateLoss()
runOnUiThread {
val fragment = supportFragmentManager.findFragmentByTag(NetworkDialogSheet.TAG)
fragment?.let {
supportFragmentManager.beginTransaction().remove(fragment).commitAllowingStateLoss()
}
}
}
override fun onStart() {
super.onStart()
NetworkProvider.addListener(this)
}
override fun onStop() {

View File

@@ -24,10 +24,11 @@ import android.content.Intent
import android.os.Build
import androidx.fragment.app.Fragment
import com.aurora.Constants
import com.aurora.extensions.getEmptyActivityBundle
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Category
import com.aurora.store.util.ViewUtil
import com.aurora.store.view.ui.details.AppDetailsActivity
import com.aurora.store.view.ui.details.DevProfileActivity
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.lang.reflect.Modifier
@@ -63,12 +64,23 @@ open class BaseFragment : Fragment() {
}
}
fun openStreamBrowseActivity(browseUrl: String) {
val intent = Intent(requireContext(), StreamBrowseActivity::class.java)
fun openStreamBrowseActivity(browseUrl: String, title: String = "") {
val intent = if (browseUrl.toLowerCase().contains("expanded"))
Intent(requireContext(), ExpandedStreamBrowseActivity::class.java)
else if (browseUrl.toLowerCase().contains("developer"))
Intent(requireContext(), DevProfileActivity::class.java)
else
Intent(requireContext(), StreamBrowseActivity::class.java)
intent.putExtra(Constants.BROWSE_EXTRA, browseUrl)
startActivity(
intent,
ViewUtil.getEmptyActivityBundle(requireContext())
)
intent.putExtra(Constants.STRING_EXTRA, title)
startActivity(intent, requireContext().getEmptyActivityBundle())
}
fun openEditorStreamBrowseActivity(browseUrl: String, title: String = "") {
val intent = Intent(requireContext(), EditorStreamBrowseActivity::class.java)
intent.putExtra(Constants.BROWSE_EXTRA, browseUrl)
intent.putExtra(Constants.STRING_EXTRA, title)
startActivity(intent, requireContext().getEmptyActivityBundle())
}
}

View File

@@ -25,8 +25,8 @@ import com.aurora.store.R
import com.aurora.store.data.model.Black
import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.databinding.ActivityGenericRecyclerBinding
import com.aurora.store.util.extensions.close
import com.aurora.store.view.epoxy.views.BlackViewModel_
import com.aurora.extensions.close
import com.aurora.store.view.epoxy.views.BlackListViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.viewmodel.all.BlacklistViewModel
@@ -94,7 +94,7 @@ class BlacklistActivity : BaseActivity() {
} else {
blackList.forEach {
add(
BlackViewModel_()
BlackListViewModel_()
.id(it.packageName.hashCode())
.black(it)
.markChecked(VM.selected.contains(it.packageName))

View File

@@ -28,7 +28,7 @@ import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.data.ViewState
import com.aurora.store.databinding.ActivityGenericRecyclerBinding
import com.aurora.store.util.extensions.close
import com.aurora.extensions.close
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.controller.CategoryCarouselController
import com.aurora.store.view.epoxy.controller.GenericCarouselController

View File

@@ -123,8 +123,6 @@ class EarlyAccessFragment : BaseFragment(), GenericCarouselController.Callbacks
override fun onHeaderClicked(streamCluster: StreamCluster) {
if (streamCluster.clusterBrowseUrl.isNotEmpty())
openStreamBrowseActivity(streamCluster.clusterBrowseUrl)
else
Toast.makeText(requireContext(), "Browse page unavailable", Toast.LENGTH_SHORT).show()
}
override fun onClusterScrolled(streamCluster: StreamCluster) {

View File

@@ -23,7 +23,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.aurora.Constants
import com.aurora.gplayapi.data.models.editor.EditorChoiceBundle
@@ -35,7 +34,7 @@ import com.aurora.store.viewmodel.editorschoice.AppEditorChoiceViewModel
import com.aurora.store.viewmodel.editorschoice.BaseEditorChoiceViewModel
import com.aurora.store.viewmodel.editorschoice.GameEditorChoiceViewModel
class EditorChoiceFragment : Fragment(), EditorChoiceController.Callbacks {
class EditorChoiceFragment : BaseFragment(), EditorChoiceController.Callbacks {
private lateinit var B: FragmentForYouBinding
private lateinit var C: EditorChoiceController
@@ -97,6 +96,9 @@ class EditorChoiceFragment : Fragment(), EditorChoiceController.Callbacks {
}
override fun onClick(editorChoiceCluster: EditorChoiceCluster) {
openEditorStreamBrowseActivity(
editorChoiceCluster.clusterBrowseUrl,
editorChoiceCluster.clusterTitle
)
}
}

View File

@@ -0,0 +1,164 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.commons
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import com.airbnb.epoxy.EpoxyModel
import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.store.databinding.ActivityGenericRecyclerBinding
import com.aurora.extensions.close
import com.aurora.store.view.epoxy.groups.CarouselHorizontalModel_
import com.aurora.store.view.epoxy.views.EditorHeadViewModel_
import com.aurora.store.view.epoxy.views.HorizontalDividerViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.details.MiniScreenshotView
import com.aurora.store.view.epoxy.views.details.MiniScreenshotViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.viewmodel.editorschoice.EditorBrowseViewModel
class EditorStreamBrowseActivity : BaseActivity() {
lateinit var B: ActivityGenericRecyclerBinding
lateinit var VM: EditorBrowseViewModel
lateinit var title: String
override fun onConnected() {
hideNetworkConnectivitySheet()
}
override fun onDisconnected() {
showNetworkConnectivitySheet()
}
override fun onReconnected() {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
B = ActivityGenericRecyclerBinding.inflate(layoutInflater)
VM = ViewModelProvider(this).get(EditorBrowseViewModel::class.java)
setContentView(B.root)
attachToolbar()
VM.liveData.observe(this, {
updateController(it)
})
intent.apply {
getStringExtra(Constants.BROWSE_EXTRA)?.let {
VM.getEditorStreamBundle(it)
}
getStringExtra(Constants.STRING_EXTRA)?.let {
B.layoutToolbarAction.txtTitle.text = it
}
}
updateController(null)
}
private fun attachToolbar() {
B.layoutToolbarAction.imgActionPrimary.setOnClickListener {
close()
}
}
private fun updateController(appList: MutableList<App>?) {
B.recycler.withModels {
setFilterDuplicates(true)
if (appList == null) {
for (i in 1..6) {
add(
AppListViewShimmerModel_()
.id(i)
)
}
} else {
appList.forEach { app ->
val screenshotsViewModels = mutableListOf<EpoxyModel<*>>()
for ((position, artwork) in app.screenshots.withIndex()) {
screenshotsViewModels.add(
MiniScreenshotViewModel_()
.id(artwork.url)
.position(position)
.artwork(artwork)
.callback(object : MiniScreenshotView.ScreenshotCallback {
override fun onClick(position: Int) {
openScreenshotActivity(app, position)
}
})
)
}
add(
AppListViewModel_()
.id("app_${app.id}")
.app(app)
.click { _ -> openDetailsActivity(app) }
)
app.editorReason?.let { editorReason ->
add(
EditorHeadViewModel_()
.id("bulletin_${app.id}")
.title(
editorReason.bulletins
.joinToString(transform = { "\n$it" })
.substringAfter(delimiter = "\n")
)
.click { _ -> openDetailsActivity(app) }
)
}
if (screenshotsViewModels.isNotEmpty()) {
add(
CarouselHorizontalModel_()
.id("screenshots_${app.id}")
.models(screenshotsViewModels)
)
}
app.editorReason?.let { editorReason ->
if (editorReason.description.isNotEmpty()) {
add(
EditorHeadViewModel_()
.id("description_${app.id}")
.title(editorReason.description)
.click { _ -> openDetailsActivity(app) }
)
}
}
add(
HorizontalDividerViewModel_()
.id("divider_${app.id}")
)
}
}
}
}
}

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