Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Sign in / Register
Toggle navigation
F
File Recovery RecycleBin
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Packages
Packages
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
wanglei
File Recovery RecycleBin
Commits
2beef962
Commit
2beef962
authored
Jul 24, 2024
by
wanglei
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
....
parent
c524029d
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
687 additions
and
74 deletions
+687
-74
AndroidManifest.xml
app/src/main/AndroidManifest.xml
+40
-40
MyApplication.kt
...ain/java/com/base/filerecoveryrecyclebin/MyApplication.kt
+4
-0
SplashActivity.kt
...om/base/filerecoveryrecyclebin/activity/SplashActivity.kt
+1
-1
BiliConstants.kt
...ava/com/base/filerecoveryrecyclebin/bean/BiliConstants.kt
+12
-0
BillingResponse.kt
...a/com/base/filerecoveryrecyclebin/bean/BillingResponse.kt
+29
-0
SubscriptionStatus.kt
...om/base/filerecoveryrecyclebin/bean/SubscriptionStatus.kt
+48
-0
BillingActivity.kt
...om/base/filerecoveryrecyclebin/billing/BillingActivity.kt
+17
-0
BillingClientLifecycle.kt
.../filerecoveryrecyclebin/billing/BillingClientLifecycle.kt
+250
-6
BillingUtilities.kt
...m/base/filerecoveryrecyclebin/billing/BillingUtilities.kt
+170
-0
BillingViewModel.kt
...m/base/filerecoveryrecyclebin/billing/BillingViewModel.kt
+49
-27
SingleLiveEvent.kt
.../com/base/filerecoveryrecyclebin/utils/SingleLiveEvent.kt
+57
-0
activity_billing.xml
app/src/main/res/layout/activity_billing.xml
+10
-0
No files found.
app/src/main/AndroidManifest.xml
View file @
2beef962
...
@@ -25,7 +25,9 @@
...
@@ -25,7 +25,9 @@
android:supportsRtl=
"true"
android:supportsRtl=
"true"
android:theme=
"@style/Theme.DataRecovery"
android:theme=
"@style/Theme.DataRecovery"
tools:targetApi=
"34"
>
tools:targetApi=
"34"
>
<activity
android:name=
".billing.BillingActivity"
android:exported=
"false"
/>
<activity
<activity
android:name=
".activity.SplashActivity"
android:name=
".activity.SplashActivity"
android:exported=
"true"
android:exported=
"true"
...
@@ -54,13 +56,12 @@
...
@@ -54,13 +56,12 @@
android:exported=
"false"
android:exported=
"false"
android:launchMode=
"singleTop"
android:launchMode=
"singleTop"
android:screenOrientation=
"portrait"
android:screenOrientation=
"portrait"
tools:ignore=
"DiscouragedApi,LockedOrientationActivity"
/>
tools:ignore=
"DiscouragedApi,LockedOrientationActivity"
/>
<!-- <activity -->
<!-- <activity-->
<!-- android:name=".activity.vip.VipActivity" -->
<!-- android:name=".activity.vip.VipActivity"-->
<!-- android:exported="false" -->
<!-- android:exported="false"-->
<!-- android:launchMode="singleTop" -->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="portrait" -->
<!-- android:screenOrientation="portrait"-->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" /> -->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" />-->
<activity
<activity
android:name=
".activity.recyclebin.RecycleBinDetailActivity"
android:name=
".activity.recyclebin.RecycleBinDetailActivity"
android:exported=
"false"
android:exported=
"false"
...
@@ -113,13 +114,12 @@
...
@@ -113,13 +114,12 @@
android:exported=
"false"
android:exported=
"false"
android:launchMode=
"singleTop"
android:launchMode=
"singleTop"
android:screenOrientation=
"portrait"
android:screenOrientation=
"portrait"
tools:ignore=
"DiscouragedApi,LockedOrientationActivity"
/>
tools:ignore=
"DiscouragedApi,LockedOrientationActivity"
/>
<!-- <activity -->
<!-- <activity-->
<!-- android:name=".activity.privacyspace.PrivacyManageActivity" -->
<!-- android:name=".activity.privacyspace.PrivacyManageActivity"-->
<!-- android:exported="false" -->
<!-- android:exported="false"-->
<!-- android:launchMode="singleTop" -->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="portrait" -->
<!-- android:screenOrientation="portrait"-->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" /> -->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" />-->
<activity
<activity
android:name=
".activity.repeat.RepeatActivity"
android:name=
".activity.repeat.RepeatActivity"
android:exported=
"false"
android:exported=
"false"
...
@@ -149,31 +149,30 @@
...
@@ -149,31 +149,30 @@
android:exported=
"false"
android:exported=
"false"
android:launchMode=
"singleTop"
android:launchMode=
"singleTop"
android:screenOrientation=
"portrait"
android:screenOrientation=
"portrait"
tools:ignore=
"DiscouragedApi,LockedOrientationActivity"
/>
tools:ignore=
"DiscouragedApi,LockedOrientationActivity"
/>
<!-- <activity -->
<!-- <activity-->
<!-- android:name=".activity.privacyspace.PrivacyPinTwoActivity" -->
<!-- android:name=".activity.privacyspace.PrivacyPinTwoActivity"-->
<!-- android:exported="false" -->
<!-- android:exported="false"-->
<!-- android:launchMode="singleTop" -->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="portrait" -->
<!-- android:screenOrientation="portrait"-->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" /> -->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" />-->
<!-- <activity -->
<!-- <activity-->
<!-- android:name=".activity.privacyspace.PrivacyPinOneActivity" -->
<!-- android:name=".activity.privacyspace.PrivacyPinOneActivity"-->
<!-- android:exported="false" -->
<!-- android:exported="false"-->
<!-- android:launchMode="singleTop" -->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="portrait" -->
<!-- android:screenOrientation="portrait"-->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" /> -->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" />-->
<!-- <activity -->
<!-- <activity-->
<!-- android:name=".activity.privacyspace.PrivacyImportActivity" -->
<!-- android:name=".activity.privacyspace.PrivacyImportActivity"-->
<!-- android:exported="false" -->
<!-- android:exported="false"-->
<!-- android:launchMode="singleTop" -->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="portrait" -->
<!-- android:screenOrientation="portrait"-->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" /> -->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" />-->
<!-- <activity -->
<!-- <activity-->
<!-- android:name=".activity.privacyspace.PrivacySpaceActivity" -->
<!-- android:name=".activity.privacyspace.PrivacySpaceActivity"-->
<!-- android:exported="false" -->
<!-- android:exported="false"-->
<!-- android:launchMode="singleTop" -->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="portrait" -->
<!-- android:screenOrientation="portrait"-->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" /> -->
<!-- tools:ignore="DiscouragedApi,LockedOrientationActivity" />-->
<activity
<activity
android:name=
".activity.recovery.FileRecoveredActivity"
android:name=
".activity.recovery.FileRecoveredActivity"
android:exported=
"false"
android:exported=
"false"
...
@@ -192,6 +191,7 @@
...
@@ -192,6 +191,7 @@
android:launchMode=
"singleTop"
android:launchMode=
"singleTop"
android:screenOrientation=
"portrait"
android:screenOrientation=
"portrait"
tools:ignore=
"DiscouragedApi,LockedOrientationActivity"
/>
tools:ignore=
"DiscouragedApi,LockedOrientationActivity"
/>
<meta-data
<meta-data
android:name=
"com.google.android.gms.ads.flag.OPTIMIZE_INITIALIZATION"
android:name=
"com.google.android.gms.ads.flag.OPTIMIZE_INITIALIZATION"
android:value=
"true"
/>
android:value=
"true"
/>
...
...
app/src/main/java/com/base/filerecoveryrecyclebin/MyApplication.kt
View file @
2beef962
...
@@ -9,6 +9,7 @@ import com.base.filerecoveryrecyclebin.activity.SplashActivity
...
@@ -9,6 +9,7 @@ import com.base.filerecoveryrecyclebin.activity.SplashActivity
import
com.base.filerecoveryrecyclebin.ads.AdmobMaxHelper
import
com.base.filerecoveryrecyclebin.ads.AdmobMaxHelper
import
com.base.filerecoveryrecyclebin.ads.admob.AdmobOpenUtils
import
com.base.filerecoveryrecyclebin.ads.admob.AdmobOpenUtils
import
com.base.filerecoveryrecyclebin.bean.ConstObject.ifAgreePrivacy
import
com.base.filerecoveryrecyclebin.bean.ConstObject.ifAgreePrivacy
import
com.base.filerecoveryrecyclebin.billing.BillingClientLifecycle
import
com.base.filerecoveryrecyclebin.fcm.FCMManager
import
com.base.filerecoveryrecyclebin.fcm.FCMManager
import
com.base.filerecoveryrecyclebin.fcm.RecoveryTimerManager
import
com.base.filerecoveryrecyclebin.fcm.RecoveryTimerManager
import
com.base.filerecoveryrecyclebin.fcm.ScreenStatusReceiver
import
com.base.filerecoveryrecyclebin.fcm.ScreenStatusReceiver
...
@@ -31,6 +32,9 @@ class MyApplication : BaseApplication() {
...
@@ -31,6 +32,9 @@ class MyApplication : BaseApplication() {
private
val
TAG
=
"MyApplication"
private
val
TAG
=
"MyApplication"
var
uuid
=
""
var
uuid
=
""
val
billingClientLifecycle
:
BillingClientLifecycle
get
()
=
BillingClientLifecycle
.
getInstance
(
this
)
companion
object
{
companion
object
{
@JvmField
@JvmField
var
PAUSED_VALUE
=
0
var
PAUSED_VALUE
=
0
...
...
app/src/main/java/com/base/filerecoveryrecyclebin/activity/SplashActivity.kt
View file @
2beef962
...
@@ -162,7 +162,7 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
...
@@ -162,7 +162,7 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
}
}
}
}
finish
()
finish
()
//
intent?.extras?.clear()
intent
?.
extras
?.
clear
()
}
}
private
fun
closeNotification
()
{
private
fun
closeNotification
()
{
...
...
app/src/main/java/com/base/filerecoveryrecyclebin/bean/BiliConstants.kt
0 → 100644
View file @
2beef962
package
com.base.filerecoveryrecyclebin.bean
object
BiliConstants
{
//Product IDs
const
val
BASIC_PRODUCT
=
"basic_subscription"
const
val
PREMIUM_PRODUCT
=
"premium_subscription"
//Tags
const
val
BASIC_MONTHLY_PLAN
=
"basicmonthly"
const
val
BASIC_YEARLY_PLAN
=
"basicyearly"
const
val
BASIC_PREPAID_PLAN_TAG
=
"prepaidbasic"
}
\ No newline at end of file
app/src/main/java/com/base/filerecoveryrecyclebin/bean/BillingResponse.kt
0 → 100644
View file @
2beef962
package
com.base.filerecoveryrecyclebin.bean
import
com.android.billingclient.api.BillingClient
class
BillingResponse
(
val
code
:
Int
)
{
val
isOk
:
Boolean
get
()
=
code
==
BillingClient
.
BillingResponseCode
.
OK
val
canFailGracefully
:
Boolean
get
()
=
code
==
BillingClient
.
BillingResponseCode
.
ITEM_ALREADY_OWNED
val
isRecoverableError
:
Boolean
get
()
=
code
in
setOf
(
BillingClient
.
BillingResponseCode
.
ERROR
,
BillingClient
.
BillingResponseCode
.
SERVICE_DISCONNECTED
,
)
val
isNonrecoverableError
:
Boolean
get
()
=
code
in
setOf
(
BillingClient
.
BillingResponseCode
.
SERVICE_UNAVAILABLE
,
BillingClient
.
BillingResponseCode
.
BILLING_UNAVAILABLE
,
BillingClient
.
BillingResponseCode
.
DEVELOPER_ERROR
,
)
val
isTerribleFailure
:
Boolean
get
()
=
code
in
setOf
(
BillingClient
.
BillingResponseCode
.
ITEM_UNAVAILABLE
,
BillingClient
.
BillingResponseCode
.
FEATURE_NOT_SUPPORTED
,
BillingClient
.
BillingResponseCode
.
ITEM_NOT_OWNED
,
BillingClient
.
BillingResponseCode
.
USER_CANCELED
,
)
}
app/src/main/java/com/base/filerecoveryrecyclebin/bean/SubscriptionStatus.kt
0 → 100644
View file @
2beef962
/*
* Copyright 2023 Google LLC. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com.base.filerecoveryrecyclebin.bean
/**
* Local subscription data. This is stored on disk in a database.
*/
data class
SubscriptionStatus
(
// Local fields.
var
primaryKey
:
Int
=
0
,
var
subscriptionStatusJson
:
String
?
=
null
,
var
subAlreadyOwned
:
Boolean
=
false
,
var
isLocalPurchase
:
Boolean
=
false
,
// Remote fields.
var
product
:
String
?
=
null
,
var
purchaseToken
:
String
?
=
null
,
var
isEntitlementActive
:
Boolean
=
false
,
var
willRenew
:
Boolean
=
false
,
var
activeUntilMillisec
:
Long
=
0
,
var
isGracePeriod
:
Boolean
=
false
,
var
isAccountHold
:
Boolean
=
false
,
var
isPaused
:
Boolean
=
false
,
var
isAcknowledged
:
Boolean
=
false
,
var
autoResumeTimeMillis
:
Long
=
0
)
{
companion
object
{
}
}
app/src/main/java/com/base/filerecoveryrecyclebin/billing/BillingActivity.kt
0 → 100644
View file @
2beef962
package
com.base.filerecoveryrecyclebin.billing
import
com.base.filerecoveryrecyclebin.databinding.ActivityBillingBinding
import
com.base.filerecoveryrecyclebin.help.BaseActivity
class
BillingActivity
:
BaseActivity
<
ActivityBillingBinding
>()
{
override
val
binding
:
ActivityBillingBinding
by
lazy
{
ActivityBillingBinding
.
inflate
(
layoutInflater
)
}
override
fun
initView
()
{
}
}
\ No newline at end of file
app/src/main/java/com/base/filerecoveryrecyclebin/billing/BillingClientLifecycle.kt
View file @
2beef962
package
com.base.filerecoveryrecyclebin.billing
package
com.base.filerecoveryrecyclebin.billing
import
android.content.Context
import
android.content.Context
import
android.util.Log
import
androidx.lifecycle.DefaultLifecycleObserver
import
androidx.lifecycle.DefaultLifecycleObserver
import
androidx.lifecycle.LifecycleOwner
import
androidx.lifecycle.MutableLiveData
import
com.android.billingclient.api.BillingClient
import
com.android.billingclient.api.BillingClient
import
com.android.billingclient.api.BillingClientStateListener
import
com.android.billingclient.api.BillingResult
import
com.android.billingclient.api.ProductDetails
import
com.android.billingclient.api.ProductDetailsResponseListener
import
com.android.billingclient.api.Purchase
import
com.android.billingclient.api.Purchase
import
com.android.billingclient.api.PurchasesResponseListener
import
com.android.billingclient.api.PurchasesUpdatedListener
import
com.android.billingclient.api.QueryPurchasesParams
import
com.base.filerecoveryrecyclebin.bean.BiliConstants
import
com.base.filerecoveryrecyclebin.bean.BillingResponse
import
com.base.filerecoveryrecyclebin.help.BaseApplication
import
kotlinx.coroutines.CoroutineScope
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.SupervisorJob
import
kotlinx.coroutines.flow.MutableStateFlow
import
kotlinx.coroutines.flow.MutableStateFlow
import
kotlinx.coroutines.flow.asStateFlow
import
kotlinx.coroutines.flow.asStateFlow
import
kotlinx.coroutines.launch
class
BillingClientLifecycle
(
class
BillingClientLifecycle
(
private
val
applicationContext
:
Context
,
private
val
applicationContext
:
Context
,
)
:
DefaultLifecycleObserver
{
private
val
coroutineScope
:
CoroutineScope
=
CoroutineScope
(
SupervisorJob
()
+
Dispatchers
.
Default
)
)
:
DefaultLifecycleObserver
,
PurchasesUpdatedListener
,
BillingClientStateListener
,
PurchasesResponseListener
,
ProductDetailsResponseListener
{
private
val
TAG
=
"BillingClientLifecycle"
/**
* Instantiate a new BillingClient instance.
*/
private
lateinit
var
billingClient
:
BillingClient
//订阅价格
private
val
_subscriptionPurchases
=
MutableStateFlow
<
List
<
Purchase
>>(
emptyList
())
private
val
_subscriptionPurchases
=
MutableStateFlow
<
List
<
Purchase
>>(
emptyList
())
val
subscriptionPurchases
=
_subscriptionPurchases
.
asStateFlow
()
val
subscriptionPurchases
=
_subscriptionPurchases
.
asStateFlow
()
//基础商品信息
val
basicSubProductWithProductDetails
=
MutableLiveData
<
ProductDetails
?>()
//高级商品信息
val
premiumSubProductWithProductDetails
=
MutableLiveData
<
ProductDetails
?>()
override
fun
onCreate
(
owner
:
LifecycleOwner
)
{
Log
.
d
(
TAG
,
"onCreate"
)
billingClient
=
BillingClient
.
newBuilder
(
BaseApplication
.
context
)
.
setListener
(
this
)
// .enablePendingPurchases() // Not used for subscriptions.
.
build
()
if
(!
billingClient
.
isReady
)
{
Log
.
d
(
TAG
,
"BillingClient: Start connection..."
)
billingClient
.
startConnection
(
this
)
}
}
override
fun
onDestroy
(
owner
:
LifecycleOwner
)
{
Log
.
d
(
TAG
,
"onDestroy"
)
if
(
billingClient
.
isReady
)
{
Log
.
d
(
TAG
,
"BillingClient can only be used once -- closing connection"
)
// BillingClient can only be used once.
// After calling endConnection(), we must create a new BillingClient.
billingClient
.
endConnection
()
}
}
/**
* 更新价格监听
* PurchasesUpdatedListener
*/
override
fun
onPurchasesUpdated
(
billingResult
:
BillingResult
,
purchases
:
MutableList
<
Purchase
>?)
{
val
responseCode
=
billingResult
.
responseCode
val
debugMessage
=
billingResult
.
debugMessage
Log
.
d
(
TAG
,
"onPurchasesUpdated: $responseCode $debugMessage"
)
when
(
responseCode
)
{
BillingClient
.
BillingResponseCode
.
OK
->
{
if
(
purchases
==
null
)
{
Log
.
d
(
TAG
,
"onPurchasesUpdated: null purchase list"
)
processPurchases
(
null
)
}
else
{
processPurchases
(
purchases
)
}
}
BillingClient
.
BillingResponseCode
.
USER_CANCELED
->
{
Log
.
i
(
TAG
,
"onPurchasesUpdated: User canceled the purchase"
)
}
@Volatile
BillingClient
.
BillingResponseCode
.
ITEM_ALREADY_OWNED
->
{
private
var
INSTANCE
:
BillingClientLifecycle
?
=
null
Log
.
i
(
TAG
,
"onPurchasesUpdated: The user already owns this item"
)
}
fun
getInstance
(
applicationContext
:
Context
):
BillingClientLifecycle
=
BillingClient
.
BillingResponseCode
.
DEVELOPER_ERROR
->
{
INSTANCE
?:
synchronized
(
this
)
{
Log
.
e
(
INSTANCE
?:
BillingClientLifecycle
(
applicationContext
).
also
{
INSTANCE
=
it
}
TAG
,
"onPurchasesUpdated: Developer error means that Google Play does "
+
"not recognize the configuration. If you are just getting started, "
+
"make sure you have configured the application correctly in the "
+
"Google Play Console. The product ID must match and the APK you "
+
"are using must be signed with release keys."
)
}
}
}
}
private
var
cachedPurchasesList
:
List
<
Purchase
>?
=
null
/**
* 重新发布价格
*/
private
fun
processPurchases
(
purchasesList
:
List
<
Purchase
>?)
{
purchasesList
?.
let
{
list
->
if
(
isUnchangedPurchaseList
(
list
))
{
Log
.
d
(
TAG
,
"processPurchases: Purchase list has not changed"
)
return
}
coroutineScope
.
launch
{
val
subscriptionPurchaseList
=
list
.
filter
{
purchase
->
purchase
.
products
.
any
{
product
->
product
in
listOf
(
BiliConstants
.
PREMIUM_PRODUCT
,
BiliConstants
.
BASIC_PRODUCT
)
}
}
_subscriptionPurchases
.
emit
(
subscriptionPurchaseList
)
}
}
}
/**
* 对比旧的价格
*/
private
fun
isUnchangedPurchaseList
(
purchasesList
:
List
<
Purchase
>):
Boolean
{
val
isUnchanged
=
purchasesList
==
cachedPurchasesList
if
(!
isUnchanged
)
{
cachedPurchasesList
=
purchasesList
}
return
isUnchanged
}
/**
* BillingClientStateListener
*/
override
fun
onBillingServiceDisconnected
()
{
Log
.
d
(
TAG
,
"onBillingServiceDisconnected"
)
}
/**
* BillingClientStateListener
*/
override
fun
onBillingSetupFinished
(
billingResult
:
BillingResult
)
{
Log
.
d
(
TAG
,
"onBillingSetupFinished"
)
val
responseCode
=
billingResult
.
responseCode
val
debugMessage
=
billingResult
.
debugMessage
Log
.
d
(
TAG
,
"onBillingSetupFinished: $responseCode $debugMessage"
)
if
(
responseCode
==
BillingClient
.
BillingResponseCode
.
OK
)
{
querySubscriptionPurchases
()
}
}
/**
* 查询订阅价格
*/
private
fun
querySubscriptionPurchases
()
{
if
(!
billingClient
.
isReady
)
{
Log
.
e
(
TAG
,
"querySubscriptionPurchases: BillingClient is not ready"
)
billingClient
.
startConnection
(
this
)
}
billingClient
.
queryPurchasesAsync
(
QueryPurchasesParams
.
newBuilder
()
.
setProductType
(
BillingClient
.
ProductType
.
SUBS
)
.
build
(),
this
)
}
companion
object
{
@Volatile
private
var
INSTANCE
:
BillingClientLifecycle
?
=
null
fun
getInstance
(
applicationContext
:
Context
):
BillingClientLifecycle
=
INSTANCE
?:
synchronized
(
this
)
{
INSTANCE
?:
BillingClientLifecycle
(
applicationContext
).
also
{
INSTANCE
=
it
}
}
}
/**
* PurchasesResponseListener
* 购买价格响应
*/
override
fun
onQueryPurchasesResponse
(
billingResult
:
BillingResult
,
purchasesList
:
MutableList
<
Purchase
>)
{
processPurchases
(
purchasesList
)
}
/**
* ProductDetailsResponseListener
* 产品细节响应
*/
override
fun
onProductDetailsResponse
(
billingResult
:
BillingResult
,
productDetailsList
:
MutableList
<
ProductDetails
>
)
{
val
response
=
BillingResponse
(
billingResult
.
responseCode
)
val
debugMessage
=
billingResult
.
debugMessage
when
{
response
.
isOk
->
{
processProductDetails
(
productDetailsList
)
}
response
.
isTerribleFailure
->
{
// These response codes are not expected.
Log
.
w
(
TAG
,
"onProductDetailsResponse - Unexpected error: ${response.code} $debugMessage"
)
}
else
->
{
Log
.
e
(
TAG
,
"onProductDetailsResponse: ${response.code} $debugMessage"
)
}
}
}
private
val
LIST_OF_SUBSCRIPTION_PRODUCTS
=
listOf
(
BiliConstants
.
BASIC_PRODUCT
,
BiliConstants
.
PREMIUM_PRODUCT
,
)
private
fun
processProductDetails
(
productDetailsList
:
MutableList
<
ProductDetails
>)
{
if
(
productDetailsList
.
isEmpty
())
{
val
productDetailsCount
=
LIST_OF_SUBSCRIPTION_PRODUCTS
.
size
Log
.
e
(
TAG
,
"productDetailsCount=$productDetailsCount, Check to see if the products you requested are correctly published in the Google Play Console."
)
postProductDetails
(
emptyList
())
}
else
{
postProductDetails
(
productDetailsList
)
}
}
private
fun
postProductDetails
(
productDetailsList
:
List
<
ProductDetails
>)
{
productDetailsList
.
forEach
{
productDetails
->
when
(
productDetails
.
productType
)
{
BillingClient
.
ProductType
.
SUBS
->
{
//订阅
if
(
productDetails
.
productId
==
BiliConstants
.
PREMIUM_PRODUCT
)
{
premiumSubProductWithProductDetails
.
postValue
(
productDetails
)
}
else
if
(
productDetails
.
productId
==
BiliConstants
.
BASIC_PRODUCT
)
{
basicSubProductWithProductDetails
.
postValue
(
productDetails
)
}
}
}
}
}
}
}
\ No newline at end of file
app/src/main/java/com/base/filerecoveryrecyclebin/billing/BillingUtilities.kt
0 → 100644
View file @
2beef962
/*
* Copyright 2018 Google LLC. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com.base.filerecoveryrecyclebin.billing
import
com.android.billingclient.api.Purchase
import
com.base.filerecoveryrecyclebin.bean.BiliConstants
import
com.base.filerecoveryrecyclebin.bean.SubscriptionStatus
/**
* Return subscription for the provided Product, if it exists.
*/
fun
subscriptionForProduct
(
subscriptions
:
List
<
SubscriptionStatus
>?,
product
:
String
):
SubscriptionStatus
?
{
subscriptions
?.
let
{
for
(
subscription
in
it
)
{
if
(
subscription
.
product
==
product
)
{
return
subscription
}
}
}
// User does not have the subscription.
return
null
}
/**
* Return purchase for the provided Product, if it exists.
*/
fun
purchaseForProduct
(
purchases
:
List
<
Purchase
>?,
product
:
String
):
Purchase
?
{
purchases
?.
let
{
for
(
purchase
in
it
)
{
if
(
purchase
.
products
[
0
]
==
product
)
{
return
purchase
}
}
}
return
null
}
/**
* This will return true if the Google Play Billing APIs have a record for the subscription.
* This will not always match the server's record of the subscription for this app user.
*
* Example: App user buys the subscription on a different device with a different Google
* account. The server will show that this app user has the subscription, even if the
* Google account on this device has not purchased the subscription.
* In this example, the method will return false.
*
* Example: The app user changes by signing out and signing into the app with a different
* email address. The server will show that this app user does not have the subscription,
* even if the Google account on this device has purchased the subscription.
* In this example, the method will return true.
*/
fun
deviceHasGooglePlaySubscription
(
purchases
:
List
<
Purchase
>?,
product
:
String
)
=
purchaseForProduct
(
purchases
,
product
)
!=
null
/**
* This will return true if the server has a record for the subscription.
* Sometimes this will return true even if the Google Play Billing APIs return false.
*
* For local purchases that are rejected by the server, this app attaches the field
* subAlreadyOwned=true to the subscription object. This means that whenever
* [deviceHasGooglePlaySubscription] returns true, and the server has processed all purchase tokens,
* we also expect this method to return true.
*
* Example: App user buys the subscription on a different device with a different Google
* account. The server will show that this app user has the subscription, even if the
* Google account on this device has not purchased the subscription.
* In this example, the method will return true, even though [deviceHasGooglePlaySubscription]
* will return false.
*
* Example: The app user changes by signing out and signing into the app with a different
* email address. The server will show that this app user does not have the subscription,
* by returning an API response indicating that it is ALREADY_OWNED.
* even if the Google account on this device has purchased the subscription.
* In this example, the method will return true. This is the same as the result from
* [deviceHasGooglePlaySubscription].
*/
fun
serverHasSubscription
(
subscriptions
:
List
<
SubscriptionStatus
>?,
product
:
String
)
=
subscriptionForProduct
(
subscriptions
,
product
)
!=
null
/**
* Returns true if the grace period option should be shown.
*/
// TODO need to be refactored like isBasicContent
fun
isGracePeriod
(
subscription
:
SubscriptionStatus
?)
=
subscription
!=
null
&&
subscription
.
isEntitlementActive
&&
subscription
.
isGracePeriod
&&
!
subscription
.
subAlreadyOwned
/**
* Returns true if the subscription restore option should be shown.
*/
// TODO need to be refactored like isBasicContent
fun
isSubscriptionRestore
(
subscription
:
SubscriptionStatus
?)
=
subscription
!=
null
&&
subscription
.
isEntitlementActive
&&
!
subscription
.
willRenew
&&
!
subscription
.
subAlreadyOwned
/**
* Returns true if the basic content should be shown.
*/
val
SubscriptionStatus
?.
isBasicContent
:
Boolean
get
()
=
this
!=
null
&&
isEntitlementActive
&&
BiliConstants
.
BASIC_PRODUCT
==
product
&&
!
subAlreadyOwned
/**
* Returns true if premium content should be shown.
*/
// TODO need to be refactored like isBasicContent
fun
isPremiumContent
(
subscription
:
SubscriptionStatus
?)
=
subscription
!=
null
&&
subscription
.
isEntitlementActive
&&
BiliConstants
.
PREMIUM_PRODUCT
==
subscription
.
product
&&
!
subscription
.
subAlreadyOwned
/**
* Returns true if account hold should be shown.
*/
// TODO need to be refactored like isBasicContent
fun
isAccountHold
(
subscription
:
SubscriptionStatus
?)
=
subscription
!=
null
&&
!
subscription
.
isEntitlementActive
&&
subscription
.
isAccountHold
&&
!
subscription
.
subAlreadyOwned
/**
* Returns true if account pause should be shown.
*/
// TODO need to be refactored like isBasicContent
fun
isPaused
(
subscription
:
SubscriptionStatus
?)
=
subscription
!=
null
&&
!
subscription
.
isEntitlementActive
&&
subscription
.
isPaused
&&
!
subscription
.
subAlreadyOwned
/**
* Returns true if the subscription is already owned and requires a transfer to this account.
*/
// TODO need to be refactored like isBasicContent
fun
isTransferRequired
(
subscription
:
SubscriptionStatus
?)
=
subscription
!=
null
&&
subscription
.
subAlreadyOwned
/**
* Returns true if the subscription is a prepraid.
*/
val
SubscriptionStatus
?.
isPrepaid
:
Boolean
get
()
=
this
!=
null
&&
!
willRenew
\ No newline at end of file
app/src/main/java/com/base/filerecoveryrecyclebin/billing/BillingViewModel.kt
View file @
2beef962
//package com.base.filerecoveryrecyclebin.billing
package
com.base.filerecoveryrecyclebin.billing
//
//import android.app.Application
import
android.app.Application
//import android.content.Context
import
android.util.Log
//import androidx.lifecycle.AndroidViewModel
import
androidx.lifecycle.AndroidViewModel
//import androidx.lifecycle.MutableLiveData
import
com.base.filerecoveryrecyclebin.MyApplication
//import com.android.billingclient.api.BillingClient
import
com.base.filerecoveryrecyclebin.bean.BiliConstants
//import com.android.billingclient.api.ProductDetails
import
com.base.filerecoveryrecyclebin.utils.SingleLiveEvent
//
//class BillingViewModel(val application: Application) : AndroidViewModel(application) {
class
BillingViewModel
(
val
application
:
Application
)
:
AndroidViewModel
(
application
)
{
//
// private lateinit var billingClient: BillingClient
private
val
TAG
=
"BillingViewModel"
//
//
// val billingClientLifecycle: BillingClientLifecycle
private
val
purchases
=
(
application
as
MyApplication
).
billingClientLifecycle
.
subscriptionPurchases
// get() = BillingClientLifecycle.getInstance(this)
private
val
basicSubProductWithProductDetails
=
//
(
application
as
MyApplication
).
billingClientLifecycle
.
basicSubProductWithProductDetails
// @Volatile
private
val
premiumSubProductWithProductDetails
=
// private var INSTANCE: BillingClientLifecycle? = null
(
application
as
MyApplication
).
billingClientLifecycle
.
premiumSubProductWithProductDetails
// fun getInstance(applicationContext: Context): BillingClientLifecycle =
// INSTANCE ?: synchronized(this) {
val
openPlayStoreSubscriptionsEvent
=
SingleLiveEvent
<
String
>()
// INSTANCE ?: BillingClientLifecycle(application.applicationContext).also { INSTANCE = it }
// }
/**
//
* Open the Play Store subscription center. If the user has exactly one product,
// val basicSubProductWithProductDetails = MutableLiveData<ProductDetails?>()
* then open the deeplink to the specific product.
//
*/
//}
fun
openPlayStoreSubscriptions
()
{
\ No newline at end of file
val
hasBasic
=
deviceHasGooglePlaySubscription
(
purchases
.
value
,
BiliConstants
.
BASIC_PRODUCT
)
val
hasPremium
=
deviceHasGooglePlaySubscription
(
purchases
.
value
,
BiliConstants
.
BASIC_PRODUCT
)
Log
.
d
(
TAG
,
"hasBasic: $hasBasic, hasPremium: $hasPremium"
)
when
{
hasBasic
&&
!
hasPremium
->
{
// If we just have a basic subscription, open the basic Product.
openPlayStoreSubscriptionsEvent
.
postValue
(
BiliConstants
.
BASIC_PRODUCT
)
}
!
hasBasic
&&
hasPremium
->
{
// If we just have a premium subscription, open the premium Product.
openPlayStoreSubscriptionsEvent
.
postValue
(
BiliConstants
.
PREMIUM_PRODUCT
)
}
else
->
{
// If we do not have an active subscription,
// or if we have multiple subscriptions, open the default subscription center.
openPlayStoreSubscriptionsEvent
.
call
()
}
}
}
}
\ No newline at end of file
app/src/main/java/com/base/filerecoveryrecyclebin/utils/SingleLiveEvent.kt
0 → 100644
View file @
2beef962
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com.base.filerecoveryrecyclebin.utils
import
androidx.annotation.MainThread
import
androidx.lifecycle.MutableLiveData
import
java.util.concurrent.atomic.AtomicBoolean
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
*
* Note that only one observer is going to be notified of changes.
*/
class
SingleLiveEvent
<
T
>
:
MutableLiveData
<
T
>()
{
private
val
pending
=
AtomicBoolean
(
false
)
@MainThread
override
fun
setValue
(
t
:
T
?)
{
pending
.
set
(
true
)
super
.
setValue
(
t
)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun
call
()
{
value
=
null
}
companion
object
{
private
const
val
TAG
=
"SingleLiveEvent"
}
}
\ No newline at end of file
app/src/main/res/layout/activity_billing.xml
0 → 100644
View file @
2beef962
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:app=
"http://schemas.android.com/apk/res-auto"
xmlns:tools=
"http://schemas.android.com/tools"
android:id=
"@+id/main"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
tools:context=
".billing.BillingActivity"
>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment