Merge pull request #11545 from nextcloud/nmc/1915-in_app_review

Nmc/1915 in app review
This commit is contained in:
Andy Scherzinger 2023-05-13 19:21:32 +02:00 committed by GitHub
commit cb08234df7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 557 additions and 5 deletions

View File

@ -366,6 +366,7 @@ dependencies {
// upon each update first test: new registration, receive push
gplayImplementation "com.google.firebase:firebase-messaging:23.1.2"
gplayImplementation 'com.google.android.play:review-ktx:2.0.0'
implementation 'com.github.nextcloud.android-common:ui:0.10.0'

View File

@ -0,0 +1,36 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.android.appReview
import androidx.appcompat.app.AppCompatActivity
import com.nextcloud.appReview.InAppReviewHelper
import com.nextcloud.client.preferences.AppPreferences
class InAppReviewHelperImpl(appPreferences: AppPreferences) :
InAppReviewHelper {
override fun resetAndIncrementAppRestartCounter() {
}
override fun showInAppReview(activity: AppCompatActivity) {
}
}

View File

@ -0,0 +1,140 @@
/*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.android.appReview
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.appReview.AppReviewShownModel
import com.nextcloud.appReview.InAppReviewHelper
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.utils.getFormattedStringDate
import com.nextcloud.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
}
}

View File

@ -0,0 +1,35 @@
/*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.android.appReview
import androidx.appcompat.app.AppCompatActivity
import com.nextcloud.appReview.InAppReviewHelper
import com.nextcloud.client.preferences.AppPreferences
class InAppReviewHelperImpl(appPreferences: AppPreferences) :
InAppReviewHelper {
override fun resetAndIncrementAppRestartCounter() {
}
override fun showInAppReview(activity: AppCompatActivity) {
}
}

View File

@ -0,0 +1,30 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.appReview
data class AppReviewShownModel(
var firstShowYear: String?,
var appRestartCount: Int,
var reviewShownCount: Int,
var lastReviewShownDate: String?
)

View File

@ -0,0 +1,48 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.appReview
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)
}

View File

@ -0,0 +1,39 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.appReview
import com.nextcloud.android.appReview.InAppReviewHelperImpl
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)
}
}

View File

@ -22,6 +22,7 @@ package com.nextcloud.client.di;
import android.app.Application;
import com.nextcloud.appReview.InAppReviewModule;
import com.nextcloud.client.appinfo.AppInfoModule;
import com.nextcloud.client.database.DatabaseModule;
import com.nextcloud.client.device.DeviceModule;
@ -53,6 +54,7 @@ import dagger.android.support.AndroidSupportInjectionModule;
ViewModelModule.class,
JobsModule.class,
IntegrationsModule.class,
InAppReviewModule.class,
ThemeModule.class,
DatabaseModule.class,
DispatcherModule.class,

View File

@ -20,9 +20,11 @@
package com.nextcloud.client.preferences;
import com.nextcloud.appReview.AppReviewShownModel;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.utils.FileSortOrder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
@ -377,4 +379,9 @@ public interface AppPreferences {
boolean isStoragePermissionRequested();
void setStoragePermissionRequested(boolean value);
void setInAppReviewData(@NonNull AppReviewShownModel appReviewShownModel);
@Nullable
AppReviewShownModel getInAppReviewData();
}

View File

@ -25,6 +25,8 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import com.google.gson.Gson;
import com.nextcloud.appReview.AppReviewShownModel;
import com.nextcloud.client.account.User;
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.account.UserAccountManagerImpl;
@ -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;
@ -92,11 +95,13 @@ public final class AppPreferencesImpl implements AppPreferences {
private static final String PREF__PIN_BRUTE_FORCE_COUNT = "pin_brute_force_count";
private static final String PREF__UID_PID = "uid_pid";
private static final String PREF__CALENDAR_AUTOMATIC_BACKUP = "calendar_automatic_backup";
private static final String PREF__CALENDAR_LAST_BACKUP = "calendar_last_backup";
private static final String PREF__PDF_ZOOM_TIP_SHOWN = "pdf_zoom_tip_shown";
private static final String PREF__STORAGE_PERMISSION_REQUESTED = "storage_permission_requested";
private static final String PREF__IN_APP_REVIEW_DATA = "in_app_review_data";
private final Context context;
private final SharedPreferences preferences;
@ -710,4 +715,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);
}
}

View File

@ -0,0 +1,79 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.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.Date
import java.util.Locale
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))
}

View File

@ -40,6 +40,7 @@ import android.os.StrictMode;
import android.text.TextUtils;
import android.view.WindowManager;
import com.nextcloud.appReview.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));

View File

@ -57,6 +57,7 @@ import android.view.WindowManager;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.appReview.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");
}

View File

@ -34,20 +34,16 @@ import com.owncloud.android.lib.resources.files.model.RemoteFile;
import com.owncloud.android.lib.resources.status.OCCapability;
import com.owncloud.android.operations.RefreshFolderOperation;
import com.owncloud.android.ui.fragment.GalleryFragment;
import com.owncloud.android.ui.fragment.SearchType;
import com.owncloud.android.utils.FileStorageUtils;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import androidx.lifecycle.Lifecycle;
public class GallerySearchTask extends AsyncTask<Void, Void, GallerySearchTask.Result> {
private final User user;
@ -220,4 +216,5 @@ public class GallerySearchTask extends AsyncTask<Void, Void, GallerySearchTask.R
this.lastTimestamp = lastTimestamp;
}
}
}
}

View File

@ -0,0 +1,36 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.android.appReview
import androidx.appcompat.app.AppCompatActivity
import com.nextcloud.appReview.InAppReviewHelper
import com.nextcloud.client.preferences.AppPreferences
class InAppReviewHelperImpl(appPreferences: AppPreferences) :
InAppReviewHelper {
override fun resetAndIncrementAppRestartCounter() {
}
override fun showInAppReview(activity: AppCompatActivity) {
}
}

View File

@ -0,0 +1,34 @@
package com.nextcloud.android.utils
import com.nextcloud.utils.getFormattedStringDate
import com.nextcloud.utils.isCurrentYear
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ExtensionsTest {
@Test
fun isCurrentYear_checkForAllConditions() {
assertFalse(System.currentTimeMillis().isCurrentYear(""))
assertFalse(System.currentTimeMillis().isCurrentYear(null))
val year2022TimeMills = 1652892268000L
assertTrue(year2022TimeMills.isCurrentYear("2022"))
assertFalse(year2022TimeMills.isCurrentYear("2021"))
}
@Test
fun getFormattedStringDate_checkForAllConditions() {
val year2022TimeMills = 1652892268000L
val actualYearValue = year2022TimeMills.getFormattedStringDate("yyyy")
assertTrue(actualYearValue == "2022")
assertFalse(actualYearValue == "2021")
assertFalse(actualYearValue == "")
val actualYearNewValue = year2022TimeMills.getFormattedStringDate("")
assertTrue(actualYearNewValue == "")
assertFalse(actualYearNewValue == "2022")
}
}

View File

@ -0,0 +1,36 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.android.appReview
import androidx.appcompat.app.AppCompatActivity
import com.nextcloud.appReview.InAppReviewHelper
import com.nextcloud.client.preferences.AppPreferences
class InAppReviewHelperImpl(appPreferences: AppPreferences) :
InAppReviewHelper {
override fun resetAndIncrementAppRestartCounter() {
}
override fun showInAppReview(activity: AppCompatActivity) {
}
}