From 12eb93c3ce9a63a5442a6549225c7fbe4d58a7db Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Thu, 11 May 2023 04:13:32 +0530 Subject: [PATCH] Implement hidden files analysis; fixes in billing system --- app/build.gradle | 1 + .../audio_player/AudioPlayerDialogActivity.kt | 4 +- .../fileutilities/docx_viewer/DocxModel.kt | 6 +- .../home_page/ui/analyse/AnalyseFragment.kt | 19 +++ .../ui/analyse/ReviewImagesFragment.kt | 11 ++ .../home_page/ui/files/FilesViewModel.kt | 26 ++- .../image_viewer/ImageViewerFragment.kt | 21 ++- .../amaze/fileutilities/utilis/CursorUtils.kt | 42 ++--- .../amaze/fileutilities/utilis/Extensions.kt | 49 ++++-- .../com/amaze/fileutilities/utilis/Utils.kt | 2 +- app/src/main/res/layout/fragment_analyse.xml | 8 + .../home_page/ui/options/Billing.kt | 151 ++++++++++++------ 12 files changed, 250 insertions(+), 90 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c52269a5..b97c06bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -280,6 +280,7 @@ dependencies { implementation 'com.burhanrashid52:photoeditor:2.0.0' implementation 'com.github.CanHub:Android-Image-Cropper:4.3.1' api 'com.github.TeamAmaze:mupdf-android-viewer:1.0.25' + implementation "androidx.documentfile:documentfile:1.0.1" //Detect memory leaks // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' diff --git a/app/src/main/java/com/amaze/fileutilities/audio_player/AudioPlayerDialogActivity.kt b/app/src/main/java/com/amaze/fileutilities/audio_player/AudioPlayerDialogActivity.kt index 7453b391..a54fd846 100644 --- a/app/src/main/java/com/amaze/fileutilities/audio_player/AudioPlayerDialogActivity.kt +++ b/app/src/main/java/com/amaze/fileutilities/audio_player/AudioPlayerDialogActivity.kt @@ -38,7 +38,7 @@ import com.amaze.fileutilities.databinding.AudioPlayerDialogActivityBinding import com.amaze.fileutilities.utilis.PreferencesConstants import com.amaze.fileutilities.utilis.Utils.Companion.showProcessingDialog import com.amaze.fileutilities.utilis.getAppCommonSharedPreferences -import com.amaze.fileutilities.utilis.getFileFromUri +import com.amaze.fileutilities.utilis.getDocumentFileFromUri import com.amaze.fileutilities.utilis.showToastInCenter import com.google.android.material.slider.Slider import com.masoudss.lib.WaveformSeekBar @@ -114,7 +114,7 @@ class AudioPlayerDialogActivity : PermissionsActivity(), IAudioPlayerInterfaceHa dialog.show() } else { dialog.dismiss() - audioUri.getFileFromUri(this)?.length()?.also { + audioUri.getDocumentFileFromUri(this)?.length()?.also { if (it > AudioPlayerInterfaceHandlerViewModel.WAVEFORM_THRESHOLD_BYTES) { viewModel.forceShowSeekbar = true } diff --git a/app/src/main/java/com/amaze/fileutilities/docx_viewer/DocxModel.kt b/app/src/main/java/com/amaze/fileutilities/docx_viewer/DocxModel.kt index 933a211b..3be93b07 100644 --- a/app/src/main/java/com/amaze/fileutilities/docx_viewer/DocxModel.kt +++ b/app/src/main/java/com/amaze/fileutilities/docx_viewer/DocxModel.kt @@ -24,7 +24,7 @@ import android.content.Context import android.net.Uri import android.os.Parcelable import com.amaze.fileutilities.utilis.BaseIntentModel -import com.amaze.fileutilities.utilis.getFileFromUri +import com.amaze.fileutilities.utilis.getDocumentFileFromUri import kotlinx.parcelize.Parcelize import java.io.InputStream @@ -42,8 +42,8 @@ data class LocalDocxModel( } override fun getName(context: Context): String { - uri.getFileFromUri(context)?.run { - return this.name + uri.getDocumentFileFromUri(context)?.name?.run { + return this } uri.path?.run { return this diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt index 8c0e450b..e4167e16 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt @@ -640,6 +640,19 @@ class AnalyseFragment : AbstractMediaFileInfoOperationsFragment() { } } } + + filesViewModel.getHiddenFilesLiveData().observe(viewLifecycleOwner) { + mediaFileInfoList -> + hiddenFilesPreview.invalidateProgress(true, null) + mediaFileInfoList?.let { + hiddenFilesPreview.invalidateProgress(false, null) + hiddenFilesPreview.loadPreviews(mediaFileInfoList) { + cleanButtonClick(it) { + filesViewModel.hiddenFilesLiveData = null + } + } + } + } if (analyseViewModel.fragmentScrollPosition != null) { Handler().postDelayed({ analyseScrollView.scrollY = analyseViewModel.fragmentScrollPosition!! @@ -988,6 +1001,12 @@ class AnalyseFragment : AbstractMediaFileInfoOperationsFragment() { this@AnalyseFragment ) } + hiddenFilesPreview.setOnClickListener { + ReviewImagesFragment.newInstance( + ReviewImagesFragment.TYPE_HIDDEN_FILES, + this@AnalyseFragment + ) + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { gamesPreview.setOnClickListener { shouldCallbackAppUninstall = false diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewImagesFragment.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewImagesFragment.kt index 1edf16b6..d22a2d80 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewImagesFragment.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewImagesFragment.kt @@ -111,6 +111,7 @@ class ReviewImagesFragment : ItemsActionBarFragment() { const val TYPE_RECENTLY_UPDATED_APPS = 28 const val TYPE_NETWORK_INTENSIVE_APPS = 29 const val TYPE_SIMILAR_IMAGES = 30 + const val TYPE_HIDDEN_FILES = 31 fun newInstance(type: Int, fragment: Fragment) { val analyseFragment = ReviewImagesFragment() @@ -648,6 +649,16 @@ class ReviewImagesFragment : ItemsActionBarFragment() { } } } + TYPE_HIDDEN_FILES -> { + filesViewModel.getHiddenFilesLiveData() + .observe(viewLifecycleOwner) { hiddenFiles -> + invalidateProcessing(true, false) + hiddenFiles?.let { + setMediaInfoList(it, false) + invalidateProcessing(false, false) + } + } + } } } diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt index 4fa41559..bdb55c2b 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt @@ -123,6 +123,7 @@ class FilesViewModel(val applicationContext: Application) : var recentlyUpdatedAppsLiveData: MutableLiveData?>? = null var junkFilesLiveData: MutableLiveData, String>?>? = null var apksLiveData: MutableLiveData?>? = null + var hiddenFilesLiveData: MutableLiveData?>? = null var gamesInstalledLiveData: MutableLiveData?>? = null var largeFilesMutableLiveData: MutableLiveData?>? = null var whatsappMediaMutableLiveData: MutableLiveData?>? = null @@ -1228,7 +1229,7 @@ class FilesViewModel(val applicationContext: Application) : ) val usageStats = Utils.getAppsUsageStats(applicationContext, days) val usageStatsPackages = usageStats.filter { - it.lastTimeUsed != 0L + it.lastTimeUsed != 0L || it.packageName == applicationContext.packageName }.map { it.packageName }.toSet() @@ -1367,7 +1368,7 @@ class FilesViewModel(val applicationContext: Application) : val usageStats = Utils.getAppsUsageStats(applicationContext, days) val freqMap = linkedMapOf() usageStats.filter { - it.lastTimeUsed != 0L + it.lastTimeUsed != 0L && it.packageName != applicationContext.packageName }.forEach { if (!freqMap.contains(it.packageName)) { freqMap[it.packageName] = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) @@ -1609,6 +1610,27 @@ class FilesViewModel(val applicationContext: Application) : return apksLiveData!! } + fun getHiddenFilesLiveData(): LiveData?> { + if (hiddenFilesLiveData == null) { + hiddenFilesLiveData = MutableLiveData() + hiddenFilesLiveData?.value = null + viewModelScope.launch(Dispatchers.IO) { + if (allMediaFilesPair == null) { + allMediaFilesPair = CursorUtils.listAll(applicationContext) + } + allMediaFilesPair?.filter { + it.title.startsWith(".") + }?.sortedBy { -1 * it.longSize }?.map { + it.extraInfo?.mediaType = MediaFileInfo.MEDIA_TYPE_UNKNOWN + it + }?.let { + hiddenFilesLiveData?.postValue(ArrayList(it)) + } + } + } + return hiddenFilesLiveData!! + } + fun getLargeFilesLiveData(): LiveData?> { if (largeFilesMutableLiveData == null) { largeFilesMutableLiveData = MutableLiveData() diff --git a/app/src/main/java/com/amaze/fileutilities/image_viewer/ImageViewerFragment.kt b/app/src/main/java/com/amaze/fileutilities/image_viewer/ImageViewerFragment.kt index 3beb522d..8638298a 100644 --- a/app/src/main/java/com/amaze/fileutilities/image_viewer/ImageViewerFragment.kt +++ b/app/src/main/java/com/amaze/fileutilities/image_viewer/ImageViewerFragment.kt @@ -32,7 +32,6 @@ import android.widget.TextView import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.res.ResourcesCompat import androidx.core.view.isVisible -import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModelProvider import com.amaze.fileutilities.R @@ -113,12 +112,17 @@ class ImageViewerFragment : AbstractMediaFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val quickViewType = requireArguments().getParcelable(VIEW_TYPE_ARGUMENT) + if (quickViewType == null) { + requireContext().showToastInCenter(getString(R.string.operation_failed)) + requireActivity().onBackPressed() + return + } if (activity is ImageViewerDialogActivity) { _binding?.layoutBottomSheet?.visibility = View.GONE _binding?.imageView?.setOnClickListener { val intent = Intent(requireContext(), ImageViewerActivity::class.java) - intent.setDataAndType(quickViewType?.uri, quickViewType?.mimeType) - if (!quickViewType?.uri?.authority.equals( + intent.setDataAndType(quickViewType.uri, quickViewType.mimeType) + if (!quickViewType.uri.authority.equals( requireContext() .packageName, true @@ -140,10 +144,11 @@ class ImageViewerFragment : AbstractMediaFragment() { } frameLayout.setProxyView(imageView) customToolbar.root.visibility = View.VISIBLE - customToolbar.title.text = DocumentFile.fromSingleUri( - requireContext(), - quickViewType!!.uri - )?.name ?: quickViewType.uri.getFileFromUri(requireContext())?.name + customToolbar.title.text = quickViewType.uri.getDocumentFileFromUri( + requireContext() + )?.name ?: quickViewType.uri.getFileFromUri( + requireContext() + )?.name customToolbar.backButton.setOnClickListener { requireActivity().onBackPressed() } @@ -165,7 +170,7 @@ class ImageViewerFragment : AbstractMediaFragment() { // viewBinding.metadata.text = result } } - quickViewType?.let { showImage(it) } + showImage(quickViewType) } private fun setupPropertiesSheet(quickViewType: LocalImageModel) { diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/CursorUtils.kt b/app/src/main/java/com/amaze/fileutilities/utilis/CursorUtils.kt index 33afc85e..21e0d6ae 100644 --- a/app/src/main/java/com/amaze/fileutilities/utilis/CursorUtils.kt +++ b/app/src/main/java/com/amaze/fileutilities/utilis/CursorUtils.kt @@ -253,18 +253,20 @@ class CursorUtils { } if (dataColumnIdx >= 0) { val path = cursor.getString(dataColumnIdx) - val f = File(path) - if (d.compareTo(Date(f.lastModified())) != 1 && !f.isDirectory) { - mediaTypeRaw = cursor.getInt(mediaColumnIdxValues.mediaTypeIdx) - val mediaFileInfo = MediaFileInfo.fromFile( - f, - queryMetaInfo( - cursor, - getMediaTypeFromSystemMediaStoreType(mediaTypeRaw), - mediaColumnIdxValues + if (path != null) { + val f = File(path) + if (d.compareTo(Date(f.lastModified())) != 1 && !f.isDirectory) { + mediaTypeRaw = cursor.getInt(mediaColumnIdxValues.mediaTypeIdx) + val mediaFileInfo = MediaFileInfo.fromFile( + f, + queryMetaInfo( + cursor, + getMediaTypeFromSystemMediaStoreType(mediaTypeRaw), + mediaColumnIdxValues + ) ) - ) - recentFiles.add(mediaFileInfo) + recentFiles.add(mediaFileInfo) + } } } } while (cursor.moveToNext()) @@ -505,32 +507,32 @@ class CursorUtils { title = cursor.getString(mediaColumnIdxValues.commonTitleIdx) } if (title == null) { - if (file == null) { + if (file == null && path != null) { file = File(path) } - title = file.name + title = file?.name } } else { - if (file == null) { + if (file == null && path != null) { file = File(path) } - title = file.name + title = file?.name } if (mediaColumnIdxValues.commonLastModifiedIdx >= 0) { lastModified = cursor.getLong(mediaColumnIdxValues.commonLastModifiedIdx) * 1000 } else { - if (file == null) { + if (file == null && path != null) { file = File(path) } - lastModified = file.lastModified() + lastModified = file?.lastModified() } if (mediaColumnIdxValues.commonSizeIdx >= 0) { size = cursor.getLong(mediaColumnIdxValues.commonSizeIdx) } else { - if (file == null) { + if (file == null && path != null) { file = File(path) } - size = file.length() + size = file?.length() } var mediaTypeNew = mediaType if (mediaColumnIdxValues.mediaTypeIdx >= 0) { @@ -540,7 +542,7 @@ class CursorUtils { return MediaFileInfo.fromFile( mediaTypeNew, id ?: -1L, - title, path, lastModified, size, + title ?: "", path, lastModified ?: 0L, size ?: 0L, context, queryMetaInfo(cursor, mediaTypeNew, mediaColumnIdxValues) ) diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/Extensions.kt b/app/src/main/java/com/amaze/fileutilities/utilis/Extensions.kt index 55b048b7..fad43e47 100644 --- a/app/src/main/java/com/amaze/fileutilities/utilis/Extensions.kt +++ b/app/src/main/java/com/amaze/fileutilities/utilis/Extensions.kt @@ -40,6 +40,7 @@ import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider +import androidx.documentfile.provider.DocumentFile import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.files.FileFilter import com.afollestad.materialdialogs.files.fileChooser @@ -115,6 +116,32 @@ fun Uri.getSiblingUriFiles(filter: (File) -> Boolean): ArrayList? { return null } +fun Uri.getDocumentFileFromUri(context: Context): DocumentFile? { + if (this == Uri.EMPTY) { + return null + } + try { + val documentFile = DocumentFile.fromSingleUri( + context, + this + ) + if (documentFile?.exists() == true) { + return documentFile + } + } catch (e: Exception) { + log.warn("failed to get document file from single uri", e) + } + try { + val treeDocumentFile = DocumentFile.fromTreeUri(context, this) + if (treeDocumentFile?.exists() == true) { + return treeDocumentFile + } + } catch (e: Exception) { + log.warn("failed to get document file from tree uri", e) + } + return null +} + fun Uri.getFileFromUri(context: Context): File? { if (this == Uri.EMPTY) { return null @@ -135,17 +162,21 @@ fun Uri.getFileFromUri(context: Context): File? { ) parcelFileDescriptor?.let { val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor - val fis = FileInputStream(fileDescriptor) - val outputFile = File( - context.cacheDir, - getContentName(context.contentResolver) ?: "sharedFile" - ) - outputStream = outputFile.outputStream() - outputStream?.let { - fis.copyTo(it) + if (fileDescriptor.valid()) { + val fis = FileInputStream(fileDescriptor) + val outputFile = File( + context.cacheDir, + getContentName(context.contentResolver) ?: "sharedFile" + ) + outputStream = outputFile.outputStream() + outputStream?.let { + fis.copyTo(it) + } + songFile = outputFile } - songFile = outputFile } + } catch (e: Exception) { + log.warn("failed to find file from uri {}", e) } finally { parcelFileDescriptor?.close() outputStream?.close() diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt b/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt index ccbc9217..93c87e82 100644 --- a/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt +++ b/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt @@ -1408,7 +1408,7 @@ class Utils { packageUid ) } catch (e: RemoteException) { - log.warn("failed to get mobile bytes for package {}", packageUid) + log.info("failed to get mobile bytes for package {}", packageUid) return 0 } var txBytes = 0L diff --git a/app/src/main/res/layout/fragment_analyse.xml b/app/src/main/res/layout/fragment_analyse.xml index 1690bc78..ca08e64b 100644 --- a/app/src/main/res/layout/fragment_analyse.xml +++ b/app/src/main/res/layout/fragment_analyse.xml @@ -117,6 +117,14 @@ app:showPreview="true" android:layout_marginTop="@dimen/material_generic" /> + ?) { if (response.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { val latestPurchase = handlePurchases(purchases) - val listener = - ConsumeResponseListener { responseCode1: BillingResult?, - purchaseToken: String? -> - // we consume the purchase, so that user can perform purchase again - responseCode1?.responseCode?.let { - responseCode -> - // mark our cloud function to update subscription status - val retrofit = Retrofit.Builder() - .baseUrl(TrialValidationApi.CLOUD_FUNCTION_BASE) - .addConverterFactory(GsonConverterFactory.create()) - .client(Utils.getOkHttpClient()) - .build() - val service = retrofit.create(TrialValidationApi::class.java) - try { - service.postValidation( - TrialValidationApi.TrialRequest( - TrialValidationApi.AUTH_TOKEN, uniqueId, - context.packageName + "_" + BuildConfig.API_REQ_TRIAL_APP_HASH, - latestPurchase?.purchaseState - ?: Trial.SUBSCRIPTION_STATUS_DEFAULT, - latestPurchase?.purchaseToken + "@gplay", isPurchaseInApp - ) - )?.execute()?.let { response -> - if (response.isSuccessful && response.body() != null) { - log.info( - "updated subscription state with " + - "response ${response.body()}" - ) - response.body() - } - } - } catch (e: Exception) { - log.warn("failed to update subscription state for trial validation", e) + latestValidPurchase = latestPurchase + latestPurchase?.let { + if (it.quantity == 1) { + // Always returns 1 for BillingClient.SkuType.SUBS + log.info("acknowledging subscription") + if (latestPurchase.purchaseState == Purchase.PurchaseState.PURCHASED) { + if (!latestPurchase.isAcknowledged) { + val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() + .setPurchaseToken(latestPurchase.purchaseToken) + billingClient!!.acknowledgePurchase( + acknowledgePurchaseParams + .build(), + acknowledgePurchaseResponseListener + ) } - if (activity?.isFinishing == false && activity?.isDestroyed == false) { - activity?.runOnUiThread { - purchaseDialog?.dismiss() - Utils.buildSubscriptionPurchasedDialog(activity!!).show() - } + } + } else { + // greater than 1 for BillingClient.SkuType.INAPP items. + log.info("consuming in app purchase") + val consumeParams = + ConsumeParams.newBuilder().setPurchaseToken( + latestPurchase + .purchaseToken + ).build() + billingClient!!.consumeAsync(consumeParams, purchaseConsumerListener) + } + } + } else { + log.warn( + "failed to acknowledge purchase with response code {} purchases {}", + response.responseCode, purchases?.size + ) + activity?.getString(R.string.operation_failed)?.let { activity?.showToastInCenter(it) } + } + } + + private val acknowledgePurchaseResponseListener = AcknowledgePurchaseResponseListener { + acknowledgePurchase(it, it.responseCode, latestValidPurchase?.purchaseToken) + } + + private val purchaseConsumerListener = + ConsumeResponseListener { responseCode1: BillingResult?, + purchaseToken: String? -> + acknowledgePurchase( + responseCode1, + latestValidPurchase?.purchaseState + ?: Purchase.PurchaseState.PURCHASED, + purchaseToken + ) + } + + /** + * subscriptionStatus = + * @see Purchase.PurchaseState.PURCHASED + * for lifetime and + * @see BillingClient.BillingResponseCode.OK for subscriptions for ease of distinction + */ + private fun acknowledgePurchase( + responseCode1: BillingResult?, + subscriptionStatus: Int, + purchaseToken: String? + ) { + // we consume the purchase, so that user can perform purchase again + responseCode1?.responseCode?.let { + responseCode -> + // mark our cloud function to update subscription status + if (responseCode == BillingClient.BillingResponseCode.OK) { + val retrofit = Retrofit.Builder() + .baseUrl(TrialValidationApi.CLOUD_FUNCTION_BASE) + .addConverterFactory(GsonConverterFactory.create()) + .client(Utils.getOkHttpClient()) + .build() + val service = retrofit.create(TrialValidationApi::class.java) + try { + service.postValidation( + TrialValidationApi.TrialRequest( + TrialValidationApi.AUTH_TOKEN, uniqueId, + context.packageName + "_" + BuildConfig.API_REQ_TRIAL_APP_HASH, + subscriptionStatus, + "$purchaseToken@gplay", isPurchaseInApp + ) + )?.execute()?.let { response -> + if (response.isSuccessful && response.body() != null) { + log.info( + "updated subscription state with " + + "response ${response.body()}" + ) + response.body() } } + } catch (e: Exception) { + log.warn("failed to update subscription state for trial validation", e) + } + if (activity?.isFinishing == false && activity?.isDestroyed == false) { + activity?.runOnUiThread { + purchaseDialog?.dismiss() + Utils.buildSubscriptionPurchasedDialog(activity!!).show() + } + } else { + // do nothing + } + } else { + log.warn( + "failed to acknowledge purchase with response {} token {}", + responseCode1.responseCode, purchaseToken + ) + activity?.getString(R.string.operation_failed)?.let { + activity?.showToastInCenter(it) } - latestPurchase?.let { - val consumeParams = - ConsumeParams.newBuilder().setPurchaseToken( - latestPurchase - .purchaseToken - ).build() - billingClient!!.consumeAsync(consumeParams, listener) } } }