Explorar o código

Google Play In-App Review.

Commit id-51336eb
from branch feature/NMCLOUD-832
Google Play In-App Review.
A200073727 %!s(int64=2) %!d(string=hai) anos
pai
achega
5787fcba14

+ 2 - 0
app/build.gradle

@@ -373,6 +373,8 @@ dependencies {
     kapt "androidx.room:room-compiler:$roomVersion"
     androidTestImplementation "androidx.room:room-testing:$roomVersion"
 
+    // Play In-App Review
+    implementation 'com.google.android.play:review-ktx:2.0.0'
     implementation "io.coil-kt:coil:2.2.2"
 }
 

+ 2 - 0
app/src/main/java/com/nextcloud/client/di/AppComponent.java

@@ -28,6 +28,7 @@ import com.nextcloud.client.device.DeviceModule;
 import com.nextcloud.client.integrations.IntegrationsModule;
 import com.nextcloud.client.jobs.JobsModule;
 import com.nextcloud.client.network.NetworkModule;
+import com.nmc.android.app_review.InAppReviewModule;
 import com.nextcloud.client.onboarding.OnboardingModule;
 import com.nextcloud.client.preferences.PreferencesModule;
 import com.owncloud.android.MainApp;
@@ -53,6 +54,7 @@ import dagger.android.support.AndroidSupportInjectionModule;
     ViewModelModule.class,
     JobsModule.class,
     IntegrationsModule.class,
+    InAppReviewModule.class,
     ThemeModule.class,
     DatabaseModule.class,
     DispatcherModule.class,

+ 3 - 1
app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java

@@ -22,7 +22,7 @@ package com.nextcloud.client.preferences;
 
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.utils.FileSortOrder;
-
+import com.nmc.android.app_review.AppReviewShownModel;
 import androidx.annotation.Nullable;
 
 /**
@@ -377,4 +377,6 @@ public interface AppPreferences {
     boolean isStoragePermissionRequested();
 
     void setStoragePermissionRequested(boolean value);
+    void setHideVideoClicked(boolean isHideVideoClicked);
+    boolean getHideVideoClicked();
 }

+ 18 - 0
app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java

@@ -25,9 +25,11 @@ import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.SharedPreferences;
 
+import com.google.gson.Gson;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.account.UserAccountManagerImpl;
+import com.nmc.android.app_review.AppReviewShownModel;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
 import com.owncloud.android.datamodel.FileDataStorageManager;
@@ -39,6 +41,7 @@ import com.owncloud.android.utils.FileSortOrder;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
@@ -115,6 +118,7 @@ public final class AppPreferencesImpl implements AppPreferences {
             this.preferences = preferences;
             this.listeners = new CopyOnWriteArraySet<>();
         }
+        private static final String PREF__IN_APP_REVIEW_DATA = "in_app_review_data";
 
         void add(@Nullable final Listener listener) {
             if (listener != null) {
@@ -710,4 +714,18 @@ public final class AppPreferencesImpl implements AppPreferences {
     public int computeBruteForceDelay(int count) {
         return (int) Math.min(count / 3d, 10);
     }
+    @Override
+    public void setInAppReviewData(@NonNull AppReviewShownModel appReviewShownModel) {
+        Gson gson = new Gson();
+        String json = gson.toJson(appReviewShownModel);
+        preferences.edit().putString(PREF__IN_APP_REVIEW_DATA, json).apply();
+    }
+
+    @Nullable
+    @Override
+    public AppReviewShownModel getInAppReviewData() {
+        Gson gson = new Gson();
+        String json = preferences.getString(PREF__IN_APP_REVIEW_DATA, "");
+        return gson.fromJson(json, AppReviewShownModel.class);
+    }
 }

+ 6 - 0
app/src/main/java/com/nmc/android/app_review/AppReviewShownModel.kt

@@ -0,0 +1,6 @@
+package com.nmc.android.app_review
+
+data class AppReviewShownModel(var firstShowYear: String?,
+    var appRestartCount: Int,
+    var reviewShownCount: Int,
+    var lastReviewShownDate: String?)

+ 26 - 0
app/src/main/java/com/nmc/android/app_review/InAppReviewHelper.kt

@@ -0,0 +1,26 @@
+package com.nmc.android.app_review
+
+
+import androidx.appcompat.app.AppCompatActivity
+
+interface InAppReviewHelper {
+
+    /**
+     * method to be called from Application onCreate() method to work properly
+     * since we have to capture the app restarts Application is the best place to do that
+     * this method will do the following:
+     * 1. Reset the @see AppReviewModel with the current year (yyyy) if the app is launched first time or if the year has changed.
+     * 2. If the year is same then it will only increment the appRestartCount
+     */
+    fun resetAndIncrementAppRestartCounter()
+
+    /**
+     * method to be called from Activity onResume() method
+     * this method will check the following conditions:
+     * 1. if the minimum app restarts happened
+     * 2. if the year is current
+     * 3. if maximum review dialog is shown or not
+     * once all the conditions satisfies it will trigger In-App Review manager to show the flow
+     */
+    fun showInAppReview(activity: AppCompatActivity)
+}

+ 117 - 0
app/src/main/java/com/nmc/android/app_review/InAppReviewHelperImpl.kt

@@ -0,0 +1,117 @@
+package com.nmc.android.app_review
+
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.gms.tasks.Task
+import com.google.android.play.core.review.ReviewException
+import com.google.android.play.core.review.ReviewInfo
+import com.google.android.play.core.review.ReviewManager
+import com.google.android.play.core.review.ReviewManagerFactory
+import com.google.android.play.core.review.model.ReviewErrorCode
+import com.nextcloud.client.preferences.AppPreferences
+import com.nmc.android.utils.getFormattedStringDate
+import com.nmc.android.utils.isCurrentYear
+import com.owncloud.android.lib.common.utils.Log_OC
+
+//Reference: https://developer.android.com/guide/playcore/in-app-review
+/**
+ * This class responsible to handle & manage in-app review related methods
+ */
+class InAppReviewHelperImpl(val appPreferences: AppPreferences) : InAppReviewHelper {
+
+    override fun resetAndIncrementAppRestartCounter() {
+        val appReviewShownModel = appPreferences.inAppReviewData
+        val currentTimeMills = System.currentTimeMillis()
+
+        if (appReviewShownModel != null) {
+            if (currentTimeMills.isCurrentYear(appReviewShownModel.firstShowYear)) {
+                appReviewShownModel.appRestartCount += 1
+                appPreferences.setInAppReviewData(appReviewShownModel)
+            } else {
+                resetReviewShownModel()
+            }
+
+        } else {
+            resetReviewShownModel()
+        }
+
+    }
+
+    private fun resetReviewShownModel() {
+        val appReviewShownModel = AppReviewShownModel(System.currentTimeMillis().getFormattedStringDate(YEAR_FORMAT),
+            1, 0, null)
+        appPreferences.setInAppReviewData(appReviewShownModel)
+    }
+
+    override fun showInAppReview(activity: AppCompatActivity) {
+        val appReviewShownModel = appPreferences.inAppReviewData
+        val currentTimeMills = System.currentTimeMillis()
+
+        appReviewShownModel?.let {
+            if (it.appRestartCount >= MIN_APP_RESTARTS_REQ
+                && currentTimeMills.isCurrentYear(it.firstShowYear)
+                && it.reviewShownCount < MAX_DISPLAY_PER_YEAR) {
+                doAppReview(activity)
+            } else {
+                Log_OC.d(TAG, "Yearly limit has been reached or minimum app restarts are not completed: $appReviewShownModel")
+            }
+        }
+    }
+
+
+    private fun doAppReview(activity: AppCompatActivity) {
+        val manager = ReviewManagerFactory.create(activity)
+        val request: Task<ReviewInfo> = manager.requestReviewFlow()
+        request.addOnCompleteListener { task ->
+            if (task.isSuccessful) {
+                // We can get the ReviewInfo object
+                val reviewInfo: ReviewInfo = task.result
+                launchAppReviewFlow(manager, activity, reviewInfo)
+            } else {
+                // There was some problem, log or handle the error code.
+                @ReviewErrorCode val reviewErrorCode = (task.exception as ReviewException).errorCode
+                Log_OC.e(TAG, "Failed to get ReviewInfo: $reviewErrorCode")
+            }
+        }
+    }
+
+    private fun launchAppReviewFlow(manager: ReviewManager,
+        activity: AppCompatActivity,
+        reviewInfo: ReviewInfo) {
+        val flow = manager.launchReviewFlow(activity, reviewInfo)
+        flow.addOnCompleteListener { _ ->
+            // The flow has finished. The API does not indicate whether the user
+            // reviewed or not, or even whether the review dialog was shown. Thus, no
+            // matter the result, we continue our app flow.
+            // Scenarios in which the flow won't shown:
+            //1. Showing dialog to frequently
+            //2. If quota is reached can be checked in official documentation
+            //3. Flow won't be shown if user has already reviewed the app. User has to delete the review from play store to show the review dialog again
+            //Link for more info: https://stackoverflow.com/a/63342266
+            Log_OC.d(TAG, "App Review flow is completed")
+        }
+
+        //on successful showing review dialog increment the count and capture the date
+        val appReviewShownModel = appPreferences.inAppReviewData
+        appReviewShownModel?.let {
+            it.appRestartCount = 0
+            it.reviewShownCount += 1
+            it.lastReviewShownDate = System.currentTimeMillis().getFormattedStringDate(DATE_TIME_FORMAT)
+            appPreferences.setInAppReviewData(it)
+        }
+
+    }
+
+
+    companion object {
+        private val TAG = InAppReviewHelperImpl::class.java.simpleName
+        const val YEAR_FORMAT = "yyyy"
+        const val DATE_TIME_FORMAT = "dd-MM-yyyy HH:mm:ss"
+        const val MIN_APP_RESTARTS_REQ = 10 //minimum app restarts required to ask the review
+        const val MAX_DISPLAY_PER_YEAR = 15 //maximum times to ask review in a year
+    }
+
+}
+
+
+
+}

+ 17 - 0
app/src/main/java/com/nmc/android/app_review/InAppReviewModule.kt

@@ -0,0 +1,17 @@
+package com.nmc.android.app_review
+
+
+import com.nextcloud.client.preferences.AppPreferences
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+@Module
+class InAppReviewModule {
+
+    @Provides
+    @Singleton
+    internal fun providesInAppReviewHelper(appPreferences: AppPreferences): InAppReviewHelper {
+        return InAppReviewHelperImpl(appPreferences)
+    }
+}

+ 54 - 0
app/src/main/java/com/nmc/android/utils/Extensions.kt

@@ -0,0 +1,54 @@
+package com.nmc.android.utils
+
+import android.text.Selection
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.TextPaint
+import android.text.method.LinkMovementMethod
+import android.text.style.ClickableSpan
+import android.view.View
+import android.widget.TextView
+import java.text.SimpleDateFormat
+import java.util.*
+
+fun TextView.makeLinks(vararg links: Pair<String, View.OnClickListener>) {
+    val spannableString = SpannableString(this.text)
+    var startIndexOfLink = -1
+    for (link in links) {
+        val clickableSpan = object : ClickableSpan() {
+            override fun updateDrawState(textPaint: TextPaint) {
+                // use this to change the link color
+                textPaint.color = textPaint.linkColor
+                // toggle below value to enable/disable
+                // the underline shown below the clickable text
+                //textPaint.isUnderlineText = true
+            }
+
+            override fun onClick(view: View) {
+                Selection.setSelection((view as TextView).text as Spannable, 0)
+                view.invalidate()
+                link.second.onClick(view)
+            }
+        }
+        startIndexOfLink = this.text.toString().indexOf(link.first, startIndexOfLink + 1)
+        spannableString.setSpan(
+            clickableSpan, startIndexOfLink, startIndexOfLink + link.first.length,
+            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+        )
+    }
+    this.movementMethod =
+        LinkMovementMethod.getInstance() // without LinkMovementMethod, link can not click
+    this.setText(spannableString, TextView.BufferType.SPANNABLE)
+}
+
+fun Long.isCurrentYear(yearToCompare: String?): Boolean {
+    val simpleDateFormat = SimpleDateFormat("yyyy", Locale.getDefault())
+    val currentYear = simpleDateFormat.format(Date(this))
+    return currentYear == yearToCompare
+}
+
+fun Long.getFormattedStringDate(format: String): String {
+    val simpleDateFormat = SimpleDateFormat(format, Locale.getDefault())
+    return simpleDateFormat.format(Date(this))
+}

+ 7 - 0
app/src/main/java/com/owncloud/android/MainApp.java

@@ -40,6 +40,7 @@ import android.os.StrictMode;
 import android.text.TextUtils;
 import android.view.WindowManager;
 
+import com.nmc.android.app_review.InAppReviewHelper;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.appinfo.AppInfo;
@@ -177,6 +178,9 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
     @Inject
     MigrationsManager migrationsManager;
 
+    @Inject
+    InAppReviewHelper inAppReviewHelper;
+
     @Inject
     PassCodeManager passCodeManager;
 
@@ -293,6 +297,9 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
 
         registerActivityLifecycleCallbacks(new ActivityInjector());
 
+        //update the app restart count when app is launched by the user
+        inAppReviewHelper.resetAndIncrementAppRestartCounter();
+
         int startedMigrationsCount = migrationsManager.startMigration();
         logger.i(TAG, String.format(Locale.US, "Started %d migrations", startedMigrationsCount));
 

+ 6 - 0
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -57,6 +57,7 @@ import android.view.WindowManager;
 
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.snackbar.Snackbar;
+import com.nmc.android.app_review.InAppReviewHelper;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.appinfo.AppInfo;
 import com.nextcloud.client.core.AsyncRunner;
@@ -241,6 +242,9 @@ public class FileDisplayActivity extends FileActivity
     @Inject
     ConnectivityService connectivityService;
 
+    @Inject
+    InAppReviewHelper inAppReviewHelper;
+
     @Inject
     FastScrollUtils fastScrollUtils;
     @Inject AsyncRunner asyncRunner;
@@ -1173,6 +1177,8 @@ public class FileDisplayActivity extends FileActivity
         if (ocFileListFragment instanceof GalleryFragment) {
             updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_gallery));
         }
+        //show in-app review dialog to user
+        inAppReviewHelper.showInAppReview(this);
 
         Log_OC.v(TAG, "onResume() end");
     }