From b575073fe87a6b489b93518bb72fc1f0b8ad45b2 Mon Sep 17 00:00:00 2001 From: A117870935 Date: Sun, 9 Jun 2024 23:12:28 +0530 Subject: [PATCH] MoEngage functionality --- app/build.gradle | 8 +- .../appReview/InAppReviewHelperImpl.kt | 4 + app/src/gplay/AndroidManifest.xml | 6 + .../appReview/InAppReviewHelperImpl.kt | 4 + .../firebase/NCFirebaseMessagingService.java | 8 + .../appReview/InAppReviewHelperImpl.kt | 4 + .../nextcloud/appReview/InAppReviewHelper.kt | 8 + .../client/jobs/AccountRemovalWork.kt | 3 + .../java/com/nextcloud/utils/ShortcutUtil.kt | 4 + .../MoEngagePropertiesHelper.kt | 26 + .../marketTracking/MoEngageSdkUtils.kt | 513 ++++++++++++++++++ .../java/com/nmc/android/utils/FileUtils.java | 171 ++++++ .../java/com/owncloud/android/MainApp.java | 11 + .../authentication/AuthenticatorActivity.java | 7 + .../operations/CreateFolderOperation.java | 4 + .../operations/UploadFileOperation.java | 5 + .../android/ui/activity/DrawerActivity.java | 12 + .../ui/activity/FileDisplayActivity.kt | 22 +- .../ui/activity/NotificationsActivity.kt | 4 + .../ui/activity/SyncedFoldersActivity.kt | 11 + ...ooseRichDocumentsTemplateDialogFragment.kt | 7 + .../ui/dialog/ChooseTemplateDialogFragment.kt | 6 + .../ui/fragment/OCFileListFragment.java | 13 + .../fragment/contactsbackup/BackupFragment.kt | 3 + .../ui/helpers/FileOperationsHelper.java | 18 + .../android/ui/trashbin/TrashbinActivity.kt | 4 + app/src/main/res/xml/backup_config.xml | 8 + app/src/main/res/xml/backup_rules.xml | 18 + .../appReview/InAppReviewHelperImpl.kt | 4 + .../appReview/InAppReviewHelperImpl.kt | 4 + gradle.properties | 1 + nmc_moengage-dependencies.gradle | 21 + settings.gradle | 12 + 33 files changed, 952 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/nmc/android/marketTracking/MoEngagePropertiesHelper.kt create mode 100644 app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt create mode 100644 app/src/main/java/com/nmc/android/utils/FileUtils.java create mode 100644 nmc_moengage-dependencies.gradle diff --git a/app/build.gradle b/app/build.gradle index 5ac6ded66a12..bcbe5cab4f10 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -53,7 +53,8 @@ apply plugin: "com.google.devtools.ksp" println "Gradle uses Java ${Jvm.current()}" - +// apply MoEngage SDK for NMC +apply from: "$rootProject.projectDir/nmc_moengage-dependencies.gradle" configurations { configureEach { exclude group: "org.jetbrains", module: "annotations-java5" // via prism4j, already using annotations explicitly @@ -114,6 +115,8 @@ android { targetSdk = 35 compileSdk = 35 + // NMC Customization + buildConfigField "String", "MOENGAGE_APP_ID", "${MOENGAGE_APP_ID}" buildConfigField "boolean", "CI", ciBuild.toString() buildConfigField "boolean", "RUNTIME_PERF_ANALYSIS", perfAnalysis.toString() @@ -445,6 +448,9 @@ dependencies { implementation "com.github.nextcloud.android-common:ui:$androidCommonLibraryVersion" implementation "io.coil-kt:coil:2.7.0" + + // NMC: dependency required to capture Advertising ID for Adjust & MoEngage SDK + implementation "com.google.android.gms:play-services-ads-identifier:18.0.1" } configurations.configureEach { diff --git a/app/src/generic/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt b/app/src/generic/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt index 34224244fc09..ee6f937491a8 100644 --- a/app/src/generic/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt +++ b/app/src/generic/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt @@ -17,4 +17,8 @@ class InAppReviewHelperImpl(appPreferences: AppPreferences) : InAppReviewHelper override fun showInAppReview(activity: AppCompatActivity) { } + + override fun performNativeReview(activity: AppCompatActivity){ + + } } diff --git a/app/src/gplay/AndroidManifest.xml b/app/src/gplay/AndroidManifest.xml index d566a24e8b90..da34891e9c6f 100644 --- a/app/src/gplay/AndroidManifest.xml +++ b/app/src/gplay/AndroidManifest.xml @@ -65,6 +65,12 @@ + + + diff --git a/app/src/gplay/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt b/app/src/gplay/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt index 01208c9a7a6c..6f4b6768de97 100644 --- a/app/src/gplay/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt +++ b/app/src/gplay/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt @@ -70,6 +70,10 @@ class InAppReviewHelperImpl(val appPreferences: AppPreferences) : InAppReviewHel } } + override fun performNativeReview(activity: AppCompatActivity) { + doAppReview(activity) + } + private fun doAppReview(activity: AppCompatActivity) { val manager = ReviewManagerFactory.create(activity) val request: Task = manager.requestReviewFlow() diff --git a/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java b/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java index ce6beea9b7e9..fbf4d585aa8c 100644 --- a/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java +++ b/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java @@ -13,6 +13,8 @@ import com.google.firebase.messaging.Constants.MessageNotificationKeys; import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; +import com.moengage.firebase.MoEFireBaseHelper; +import com.moengage.pushbase.MoEPushHelper; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.jobs.NotificationWork; @@ -82,6 +84,12 @@ public void handleIntent(Intent intent) { @Override public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { Log_OC.d(TAG, "onMessageReceived"); + // NMC: check and pass the Notification payload to MoEngage to handle it + if (MoEPushHelper.getInstance().isFromMoEngagePlatform(remoteMessage.getData())) { + MoEFireBaseHelper.getInstance().passPushPayload(getApplicationContext(), remoteMessage.getData()); + return; + } + final Map data = remoteMessage.getData(); final String subject = data.get(NotificationWork.KEY_NOTIFICATION_SUBJECT); final String signature = data.get(NotificationWork.KEY_NOTIFICATION_SIGNATURE); diff --git a/app/src/huawei/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt b/app/src/huawei/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt index 34224244fc09..ee6f937491a8 100644 --- a/app/src/huawei/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt +++ b/app/src/huawei/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt @@ -17,4 +17,8 @@ class InAppReviewHelperImpl(appPreferences: AppPreferences) : InAppReviewHelper override fun showInAppReview(activity: AppCompatActivity) { } + + override fun performNativeReview(activity: AppCompatActivity){ + + } } diff --git a/app/src/main/java/com/nextcloud/appReview/InAppReviewHelper.kt b/app/src/main/java/com/nextcloud/appReview/InAppReviewHelper.kt index 34a054b14c13..65c7c8c07025 100644 --- a/app/src/main/java/com/nextcloud/appReview/InAppReviewHelper.kt +++ b/app/src/main/java/com/nextcloud/appReview/InAppReviewHelper.kt @@ -30,4 +30,12 @@ interface InAppReviewHelper { * once all the conditions satisfies it will trigger In-App Review manager to show the flow */ fun showInAppReview(activity: AppCompatActivity) + + /** + * NMC customization + * method to perform direct native review without the logic of app launch count + * this will be triggered when MoEngage push comes to show native rating + * === DO NOT CALL THIS FUNCTION DIRECTLY FOR ANY OTHER USE CASE === + */ + fun performNativeReview(activity: AppCompatActivity) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt index ebcb5f7dd4d3..e0958602f541 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt @@ -23,6 +23,7 @@ import com.nextcloud.client.core.Clock import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.common.NextcloudClient import com.owncloud.android.MainApp +import com.nmc.android.marketTracking.MoEngageSdkUtils import com.owncloud.android.R import com.owncloud.android.datamodel.ArbitraryDataProvider import com.owncloud.android.datamodel.ArbitraryDataProviderImpl @@ -142,6 +143,8 @@ class AccountRemovalWork( if (userRemoved) { eventBus.post(AccountRemovedEvent()) + // NMC: track user logout + MoEngageSdkUtils.trackUserLogout(context) } return Result.success() diff --git a/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt b/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt index f0800cc1402d..924a06968857 100644 --- a/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt +++ b/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt @@ -22,6 +22,7 @@ import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toDrawable import com.nextcloud.client.account.User +import com.nmc.android.marketTracking.MoEngageSdkUtils import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.SyncedFolderProvider @@ -92,6 +93,9 @@ class ShortcutUtil @Inject constructor(private val mContext: Context) { pinShortcutInfo, successCallback.intentSender ) + + // NMC: track pin to home screen event + MoEngageSdkUtils.trackPinHomeScreenEvent(mContext, file) } } diff --git a/app/src/main/java/com/nmc/android/marketTracking/MoEngagePropertiesHelper.kt b/app/src/main/java/com/nmc/android/marketTracking/MoEngagePropertiesHelper.kt new file mode 100644 index 000000000000..c74ec8af33c0 --- /dev/null +++ b/app/src/main/java/com/nmc/android/marketTracking/MoEngagePropertiesHelper.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nmc.android.marketTracking + +enum class EventFileType(val fileType: String) { + PHOTO("foto"), + SCAN("scan"), + VIDEO("video"), + AUDIO("audio"), + TEXT("text"), + PDF("pdf"), + DOCUMENT("docx"), + SPREADSHEET("xlsx"), + PRESENTATION("pptx"), + OTHER("other"), // default +} + +enum class EventFolderType(val folderType: String) { + ENCRYPTED("encrypted"), + NOT_ENCRYPTED("not encrypted") +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt new file mode 100644 index 000000000000..28d3ad347934 --- /dev/null +++ b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt @@ -0,0 +1,513 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nmc.android.marketTracking + +import android.Manifest +import android.app.Application +import android.content.Context +import android.os.Build +import com.moengage.core.DataCenter +import com.moengage.core.MoECoreHelper +import com.moengage.core.MoEngage +import com.moengage.core.Properties +import com.moengage.core.analytics.MoEAnalyticsHelper +import com.moengage.core.config.NotificationConfig +import com.moengage.core.enableAdIdTracking +import com.moengage.core.enableAndroidIdTracking +import com.moengage.core.model.AppStatus +import com.moengage.inapp.MoEInAppHelper +import com.moengage.inapp.listeners.OnClickActionListener +import com.moengage.inapp.model.ClickData +import com.moengage.inapp.model.actions.CustomAction +import com.moengage.pushbase.MoEPushHelper +import com.nextcloud.client.account.User +import com.nextcloud.common.NextcloudClient +import com.nextcloud.utils.extensions.getFormattedStringDate +import com.nmc.android.utils.FileUtils +import com.owncloud.android.BuildConfig +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.Template +import com.owncloud.android.lib.common.OwnCloudClientFactory +import com.owncloud.android.lib.common.Quota +import com.owncloud.android.lib.common.UserInfo +import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.PermissionUtil +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.round + +object MoEngageSdkUtils { + + private const val USER_PROPERTIES__STORAGE_CAPACITY = "storage_capacity" // in GB + private const val USER_PROPERTIES__STORAGE_USED = "storage_used" // % of storage used + private const val USER_PROPERTIES__CONTACT_BACKUP = "contact_backup_on" + private const val USER_PROPERTIES__AUTO_UPLOAD = "auto_upload_on" + private const val USER_PROPERTIES__APP_VERSION = "app_version" + + private const val EVENT__ACTION_BUTTON = "action_button_clicked" // when user clicks on fab + button + private const val EVENT__UPLOAD_FILE = + "upload_file" // when user uploads any file (not applicable for folder) from other apps + private const val EVENT__CREATE_FILE = "create_file" // when user creates any file in app + private const val EVENT__CREATE_FOLDER = "create_folder" + private const val EVENT__ADD_FAVORITE = "add_favorite" + private const val EVENT__SHARE_FILE = "share_file" // when user share any file using link + private const val EVENT__OFFLINE_AVAILABLE = "offline_available" + private const val EVENT__PIN_TO_HOME_SCREEN = "pin_to_homescreen" + private const val EVENT__ONLINE_OFFICE_USED = "online_office_used" // when user opens any office files + + // screen view events when user open specific screen + private const val SCREEN_EVENT__FAVOURITES = "favorites" + private const val SCREEN_EVENT__MEDIA = "medien" + private const val SCREEN_EVENT__OFFLINE_FILES = "offline_files" + private const val SCREEN_EVENT__SHARED = "shared" + private const val SCREEN_EVENT__DELETED_FILES = "deleted_files" + private const val SCREEN_EVENT__NOTIFICATIONS = "notifications" + + // properties attributes key + private const val PROPERTIES__FILE_TYPE = "file_type" + private const val PROPERTIES__FOLDER_TYPE = "folder_type" + private const val PROPERTIES__FILE_SIZE = "file_size" // in MB + private const val PROPERTIES__CREATION_DATE = "creation_date" // yyyy-MM-dd + private const val PROPERTIES__UPLOAD_DATE = "upload_date" // // yyyy-MM-dd + + private const val KILOBYTE: Long = 1024 + private const val MEGABYTE = KILOBYTE * 1024 + private const val GIGABYTE = MEGABYTE * 1024 + + // app version code for which user attributes need to track + // this should be the previous version before MoEngage is included + // Note: will be removed in future once MoEngage feature rolled out to all devices + private const val OLD_VERSION_CODE = 7_29_00 + + private const val DATE_FORMAT = "yyyy-MM-dd" + + // maximum post notification permission retry count + private const val PUSH_PERMISSION_REQUEST_RETRY_COUNT = 2 + + // key for custom action when rating campaign is triggered + private const val MOE_CUSTOM_ACTION_NATIVE_RATING_KEY = "show-native-rating" + + @JvmStatic + fun initMoEngageSDK(application: Application) { + val moEngage = MoEngage.Builder(application, BuildConfig.MOENGAGE_APP_ID, DataCenter.DATA_CENTER_2) + .configureNotificationMetaData( + NotificationConfig( + R.drawable.notification_icon, + R.drawable.notification_icon, + R.color.primary, + false + ) + ) + .build() + MoEngage.initialiseDefaultInstance(moEngage) + + updatePostNotificationsPermission(application) + + enableDeviceIdentifierTracking(application) + + // track app version at app launch + trackAppVersion(application) + } + + // for NMC the default privacy tracking consent is always taken from users + // so the tracking will always be enabled for MoEngage + private fun enableDeviceIdentifierTracking(context: Context) { + enableAndroidIdTracking(context) + enableAdIdTracking(context) + } + + private fun trackAppVersion(context: Context) { + MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__APP_VERSION, BuildConfig.VERSION_NAME) + } + + /** + * method to check if a user updated the app from older version where MoEngage was not included + * if user app version is old and is logged in then we have to auto capture the user attributes to map the events + * Note: Will be removed when MoEngage will be rolled out to all versions + */ + @JvmStatic + fun captureUserAttrsForOldAppVersion( + context: Context, + lastSeenVersionCode: Int, + user: User + ) { + if (lastSeenVersionCode in 1..OLD_VERSION_CODE && !user.isAnonymous) { + fetchUserInfo(context, user) + } + + // if user is not logged in for older app versions then nothing to do + // as the events will be captured after successful login + } + + @JvmStatic + fun trackAppInstallOrUpdate(context: Context, lastSeenVersionCode: Int) { + if (lastSeenVersionCode <= 0) { + trackAppInstall(context) + } else if (lastSeenVersionCode < BuildConfig.VERSION_CODE) { + trackAppUpdate(context) + } + // For same version code no event has to send + } + + private fun trackAppInstall(context: Context) { + // For Fresh Install of App + MoEAnalyticsHelper.setAppStatus(context, AppStatus.INSTALL) + } + + private fun trackAppUpdate(context: Context) { + // For Existing user who has updated the app + MoEAnalyticsHelper.setAppStatus(context, AppStatus.UPDATE) + } + + @JvmStatic + fun trackUserLogin(context: Context, userInfo: UserInfo) { + userInfo.id?.let { + MoEAnalyticsHelper.setUniqueId(context, it) + } + userInfo.displayName?.let { + MoEAnalyticsHelper.setUserName(context, it) + } + userInfo.email?.let { + MoEAnalyticsHelper.setEmailId(context, it) + } + trackQuotaStorage(context, userInfo.quota) + } + + @JvmStatic + fun trackQuotaStorage(context: Context, quota: Quota?) { + quota?.let { + val totalQuota = if (it.quota > 0) { + bytesToGB(it.total).toString() + } else { + it.total.toString() + } + // capture storage capacity + MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__STORAGE_CAPACITY, totalQuota) + + val usedSpace = ceil(it.relative).toInt() + // capture storage used + MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__STORAGE_USED, usedSpace) + } + } + + @JvmStatic + fun trackContactBackup(context: Context, isEnabled: Boolean) { + MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__CONTACT_BACKUP, isEnabled) + } + + @JvmStatic + fun trackAutoUpload(context: Context, syncedFoldersCount: Int) { + // since multiple folders can be enabled for auto upload + // user can add or remove a folder anytime, and we don't have single flag to check if auto upload is enabled + // so we have to check the count and if there are folders more than 0 i.e. auto upload is enabled + MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__AUTO_UPLOAD, syncedFoldersCount > 0) + } + + @JvmStatic + fun trackActionButtonEvent(context: Context) { + MoEAnalyticsHelper.trackEvent(context, EVENT__ACTION_BUTTON, Properties()) + } + + @JvmStatic + fun trackUploadFileEvent(context: Context, file: OCFile, originalStoragePath: String) { + if (file.isFolder) return + + MoEAnalyticsHelper.trackEvent( + context, EVENT__UPLOAD_FILE, getCommonProperties( + file, + FileUtils.isScannedFiles(context, originalStoragePath) + ) + ) + } + + @JvmStatic + fun trackCreateFileEvent(context: Context, file: OCFile, type: Template.Type? = null) { + if (file.isFolder) return + + val properties = Properties() + properties.addAttribute(PROPERTIES__FILE_TYPE, getOfficeFileType(type) { getFileType(file) }.fileType) + properties.addAttribute(PROPERTIES__FILE_SIZE, bytesToMBInDecimal(file.fileLength).toString()) + properties.addAttribute( + PROPERTIES__CREATION_DATE, + // using modification timestamp as this will always have value + file.modificationTimestamp.getFormattedStringDate(DATE_FORMAT) + ) + + MoEAnalyticsHelper.trackEvent(context, EVENT__CREATE_FILE, properties) + } + + @JvmStatic + fun trackCreateFolderEvent(context: Context, file: OCFile) { + if (!file.isFolder) return + + val properties = Properties() + properties.addAttribute(PROPERTIES__FOLDER_TYPE, getFolderType(file).folderType) + properties.addAttribute( + PROPERTIES__CREATION_DATE, + // using modification timestamp because for folder creationTimeStamp is always 0 + file.modificationTimestamp.getFormattedStringDate(DATE_FORMAT) + ) + + MoEAnalyticsHelper.trackEvent(context, EVENT__CREATE_FOLDER, properties) + } + + @JvmStatic + fun trackAddFavoriteEvent(context: Context, file: OCFile) { + if (file.isFolder) return + + MoEAnalyticsHelper.trackEvent(context, EVENT__ADD_FAVORITE, getCommonProperties(file)) + } + + @JvmStatic + fun trackShareFileEvent(context: Context, file: OCFile) { + if (file.isFolder) return + + MoEAnalyticsHelper.trackEvent(context, EVENT__SHARE_FILE, getCommonProperties(file)) + } + + @JvmStatic + fun trackOfflineAvailableEvent(context: Context, file: OCFile) { + if (file.isFolder) return + + MoEAnalyticsHelper.trackEvent(context, EVENT__OFFLINE_AVAILABLE, getCommonProperties(file)) + } + + @JvmStatic + fun trackPinHomeScreenEvent(context: Context, file: OCFile) { + if (file.isFolder) return + + MoEAnalyticsHelper.trackEvent(context, EVENT__PIN_TO_HOME_SCREEN, getCommonProperties(file)) + } + + @JvmStatic + fun trackOnlineOfficeUsedEvent(context: Context, file: OCFile) { + if (file.isFolder) return + + MoEAnalyticsHelper.trackEvent(context, EVENT__ONLINE_OFFICE_USED, Properties()) + } + + @JvmStatic + fun trackFavouriteScreenEvent(context: Context) { + MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__FAVOURITES, Properties()) + } + + @JvmStatic + fun trackMediaScreenEvent(context: Context) { + MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__MEDIA, Properties()) + } + + @JvmStatic + fun trackOfflineFilesScreenEvent(context: Context) { + MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__OFFLINE_FILES, Properties()) + } + + @JvmStatic + fun trackSharedScreenEvent(context: Context) { + MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__SHARED, Properties()) + } + + @JvmStatic + fun trackDeletedFilesScreenEvent(context: Context) { + MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__DELETED_FILES, Properties()) + } + + @JvmStatic + fun trackNotificationsScreenEvent(context: Context) { + MoEAnalyticsHelper.trackEvent(context, SCREEN_EVENT__NOTIFICATIONS, Properties()) + } + + @JvmStatic + fun trackUserLogout(context: Context) { + MoECoreHelper.logoutUser(context) + } + + private fun getCommonProperties(file: OCFile, isScan: Boolean = false): Properties { + val properties = Properties() + properties.addAttribute(PROPERTIES__FILE_TYPE, getFileType(file, isScan).fileType) + properties.addAttribute(PROPERTIES__FILE_SIZE, bytesToMBInDecimal(file.fileLength).toString()) + properties.addAttribute( + PROPERTIES__CREATION_DATE, + // using modification timestamp as this will always have value + file.modificationTimestamp.getFormattedStringDate(DATE_FORMAT) + ) + properties.addAttribute( + PROPERTIES__UPLOAD_DATE, + (file.uploadTimestamp * 1000L).getFormattedStringDate(DATE_FORMAT) + ) + return properties + } + + private fun bytesToGB(bytes: Long): Int { + return floor((bytes / GIGABYTE).toDouble()).toInt() + } + + private fun bytesToMBInDecimal(bytes: Long): Double { + val mb = bytes.toDouble() / MEGABYTE + return round((mb * 10)) / 10 // Round down to 1 decimal place + } + + private fun getFileType(file: OCFile, isScan: Boolean = false): EventFileType { + // if upload is happening through scan then no need to check mime type + // just set SCAN as type and send event + if (isScan) return EventFileType.SCAN + + return when { + MimeTypeUtil.isImage(file) -> { + EventFileType.PHOTO + } + + MimeTypeUtil.isVideo(file) -> { + EventFileType.VIDEO + } + + MimeTypeUtil.isAudio(file) -> { + EventFileType.AUDIO + } + + MimeTypeUtil.isPDF(file) -> { + EventFileType.PDF + } + + MimeTypeUtil.isText(file) -> { + EventFileType.TEXT + } + + else -> { + EventFileType.OTHER + } + } + } + + private fun getOfficeFileType( + type: Template.Type?, + getFileType: () -> EventFileType + ): EventFileType { + return when (type) { + Template.Type.DOCUMENT -> { + EventFileType.DOCUMENT + } + + Template.Type.SPREADSHEET -> { + EventFileType.SPREADSHEET + } + + Template.Type.PRESENTATION -> { + EventFileType.PRESENTATION + } + + else -> { + getFileType() + } + } + } + + private fun getFolderType(file: OCFile): EventFolderType { + return if (file.isEncrypted) { + EventFolderType.ENCRYPTED + } else { + EventFolderType.NOT_ENCRYPTED + } + } + + private fun fetchUserInfo(context: Context, user: User) { + val t = Thread(Runnable { + val nextcloudClient: NextcloudClient + try { + nextcloudClient = OwnCloudClientFactory.createNextcloudClient( + user, + context + ) + } catch (e: AccountUtils.AccountNotFoundException) { + Log_OC.e(this, "Error retrieving user info", e) + return@Runnable + } catch (e: SecurityException) { + Log_OC.e(this, "Error retrieving user info", e) + return@Runnable + } + + val result = GetUserInfoRemoteOperation().execute(nextcloudClient) + if (result.isSuccess && result.resultData != null) { + val userInfo = result.resultData + + trackUserLogin(context, userInfo) + } else { + Log_OC.d(this, result.logMessage) + } + }) + + t.start() + } + + @JvmStatic + fun updatePostNotificationsPermission(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val isGranted = PermissionUtil.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) + + MoEPushHelper.getInstance().pushPermissionResponse(context, isGranted) + + if (!isGranted) { + MoEPushHelper.getInstance() + .updatePushPermissionRequestCount(context, PUSH_PERMISSION_REQUEST_RETRY_COUNT) + } + } else { + MoEPushHelper.getInstance().setUpNotificationChannels(context) + } + } + + /** + * function should be called from onStart() of Activity + * or onResume() of Fragment + */ + @JvmStatic + fun displayInAppNotification(context: Context) { + MoEInAppHelper.getInstance().showInApp(context) + } + + /** + * To show In-App in both Portrait and Landscape mode properly + * when Activity is handling Config changes by itself + * call this function from onConfigurationChanged() + */ + @JvmStatic + fun handleConfigChangesForInAppNotification() { + MoEInAppHelper.getInstance().onConfigurationChanged() + } + + @JvmStatic + fun handleCustomActionCallback(actionCallback: OnHandleCustomActionCallback?) { + MoEInAppHelper.getInstance().setClickActionListener(object : OnClickActionListener { + override fun onClick(clickData: ClickData): Boolean { + Log_OC.d("MoEngageSDK", "Click data: $clickData") + val maps = (clickData.action as CustomAction).keyValuePairs + maps?.let { + if (it.containsKey(MOE_CUSTOM_ACTION_NATIVE_RATING_KEY)) { + val value = it.getValue(MOE_CUSTOM_ACTION_NATIVE_RATING_KEY) + if (value == "true") { + Log_OC.d("MoEngageSDK", "$MOE_CUSTOM_ACTION_NATIVE_RATING_KEY matches and its true") + actionCallback?.handleAction(CustomActionType.RATING) + return true + } + } + } + return false + } + }) + } + + enum class CustomActionType { + RATING + } + + interface OnHandleCustomActionCallback { + fun handleAction(actionType: CustomActionType) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/FileUtils.java b/app/src/main/java/com/nmc/android/utils/FileUtils.java new file mode 100644 index 000000000000..97496b9a5f59 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/FileUtils.java @@ -0,0 +1,171 @@ +package com.nmc.android.utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Environment; +import android.text.TextUtils; + +import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.ui.helpers.FileOperationsHelper; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import androidx.annotation.NonNull; + +// TODO: 06/24/23 Migrate to FileUtil once Rotate PR is upstreamed and merged by NC +public class FileUtils { + private static final String TAG = FileUtils.class.getSimpleName(); + + private static final String SCANS_FILE_DIR = "Scans"; + private static final String SCANNED_FILE_PREFIX = "scan_"; + + // while generating pdf using Scanbot it provide us following path: + // /scanbot-sdk/snapping_documents/.pdf + // this path will help us to differentiate if pdf file is generating by scanbot + private static final String SCANBOT_PDF_LOCAL_PATH = "/scanbot-sdk/snapping_documents/"; + private static final int JPG_FILE_TYPE = 1; + private static final int PNG_FILE_TYPE = 2; + + public static File saveJpgImage(Context context, Bitmap bitmap, String imageName, int quality) { + return createFileAndSaveImage(context, bitmap, imageName, quality, JPG_FILE_TYPE); + } + + public static File savePngImage(Context context, Bitmap bitmap, String imageName, int quality) { + return createFileAndSaveImage(context, bitmap, imageName, quality, PNG_FILE_TYPE); + } + + private static File createFileAndSaveImage(Context context, Bitmap bitmap, String imageName, int quality, + int fileType) { + File file = fileType == PNG_FILE_TYPE ? getPngImageName(context, imageName) : getJpgImageName(context, + imageName); + return saveImage(file, bitmap, quality, fileType); + } + + private static File saveImage(File file, Bitmap bitmap, int quality, int fileType) { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, bos); + byte[] bitmapData = bos.toByteArray(); + + FileOutputStream fileOutputStream = new FileOutputStream(file); + fileOutputStream.write(bitmapData); + fileOutputStream.flush(); + fileOutputStream.close(); + return file; + } catch (Exception e) { + Log_OC.e(TAG, " Failed to save image : " + e.getLocalizedMessage()); + return null; + } + } + + private static File getJpgImageName(Context context, String imageName) { + File imageFile = getOutputMediaFile(context); + if (!TextUtils.isEmpty(imageName)) { + return new File(imageFile.getPath() + File.separator + imageName + ".jpg"); + } else { + return new File(imageFile.getPath() + File.separator + "IMG_" + FileOperationsHelper.getCapturedImageName()); + } + } + + private static File getPngImageName(Context context, String imageName) { + File imageFile = getOutputMediaFile(context); + if (!TextUtils.isEmpty(imageName)) { + return new File(imageFile.getPath() + File.separator + imageName + ".png"); + } else { + return new File(imageFile.getPath() + File.separator + "IMG_" + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".png")); + } + } + + private static File getTextFileName(Context context, String fileName) { + File txtFileName = getOutputMediaFile(context); + if (!TextUtils.isEmpty(fileName)) { + return new File(txtFileName.getPath() + File.separator + fileName + ".txt"); + } else { + return new File(txtFileName.getPath() + File.separator + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".txt")); + } + } + + public static File getPdfFileName(Context context, String fileName) { + File pdfFileName = getOutputMediaFile(context); + if (!TextUtils.isEmpty(fileName)) { + return new File(pdfFileName.getPath() + File.separator + fileName + ".pdf"); + } else { + return new File(pdfFileName.getPath() + File.separator + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".pdf")); + } + } + + public static String scannedFileName() { + return SCANNED_FILE_PREFIX + new SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US).format(new Date()); + } + + public static File getOutputMediaFile(Context context) { + File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), SCANS_FILE_DIR); + if (!file.exists()) { + file.mkdir(); + } + return file; + } + + public static Bitmap convertFileToBitmap(File file) { + String filePath = file.getPath(); + Bitmap bitmap = BitmapFactory.decodeFile(filePath); + return bitmap; + } + + public static File writeTextToFile(Context context, String textToWrite, String fileName) { + File file = getTextFileName(context, fileName); + try { + FileWriter fileWriter = new FileWriter(file); + fileWriter.write(textToWrite); + fileWriter.flush(); + fileWriter.close(); + return file; + } catch (IOException e) { + //e.printStackTrace(); + Log_OC.e(TAG, "Failed to write file : " + e.toString()); + } + return null; + + } + + /** + * method to check if uploading file is from Scans or not + * + * @param path local path of the uploading file + */ + public static boolean isScannedFiles(@NonNull Context context, @NonNull String path) { + if (path.isEmpty()) { + return false; + } + + return (path.contains(getOutputMediaFile(context).getPath()) || path.contains(SCANBOT_PDF_LOCAL_PATH)); + } + + /** + * delete all the files inside the pictures directory + * this directory is getting used to store the scanned images temporarily till they uploaded to cloud + * the scanned files after downloading will get deleted by UploadWorker but in case some files still there + * then we have to delete it when user do logout from the app + * @param context + */ + public static void deleteFilesFromPicturesDirectory(Context context) { + File getFileDirectory = getOutputMediaFile(context); + if (getFileDirectory.isDirectory()) { + File[] fileList = getFileDirectory.listFiles(); + if (fileList != null && fileList.length > 0) { + for (File file : fileList) { + file.delete(); + } + } + } + } + +} diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 563d009dbbe6..f716dcef8a4d 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -63,6 +63,7 @@ import com.nextcloud.receiver.NetworkChangeReceiver; import com.nextcloud.utils.extensions.ContextExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; +import com.nmc.android.marketTracking.MoEngageSdkUtils; import com.nmc.android.ui.LauncherActivity; import com.owncloud.android.authentication.PassCodeManager; import com.owncloud.android.datamodel.ArbitraryDataProvider; @@ -314,6 +315,9 @@ public void onCreate() { registerActivityLifecycleCallbacks(new ActivityInjector()); + // NMC: init MoEngage SDK + initMoEngage(); + // NMC: end //update the app restart count when app is launched by the user inAppReviewHelper.resetAndIncrementAppRestartCounter(); @@ -996,6 +1000,13 @@ private static void cleanOldEntries(Clock clock) { } } + private void initMoEngage(){ + MoEngageSdkUtils.initMoEngageSDK(this); + MoEngageSdkUtils.trackAppInstallOrUpdate(this, preferences.getLastSeenVersionCode()); + MoEngageSdkUtils.captureUserAttrsForOldAppVersion(this, preferences.getLastSeenVersionCode(), + accountManager.getUser()); + } + @Override public AndroidInjector androidInjector() { return dispatchingAndroidInjector; diff --git a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java index ee3c505ba035..1a48c19971a7 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -66,6 +66,7 @@ import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.BuildConfig; import com.owncloud.android.MainApp; +import com.nmc.android.marketTracking.MoEngageSdkUtils; import com.owncloud.android.R; import com.owncloud.android.databinding.AccountSetupBinding; import com.owncloud.android.databinding.AccountSetupWebviewBinding; @@ -1295,6 +1296,8 @@ public void onAuthenticatorTaskCallback(RemoteOperationResult result) if (success) { accountManager.setCurrentOwnCloudAccount(mAccount.name); getUserCapabilitiesAndFinish(); + // NMC: MoEngage user login event tracking + trackUserLoginEvent(result.getResultData()); } else { accountSetupBinding = AccountSetupBinding.inflate(getLayoutInflater()); setContentView(accountSetupBinding.getRoot()); @@ -1339,6 +1342,10 @@ public void onAuthenticatorTaskCallback(RemoteOperationResult result) } } + private void trackUserLoginEvent(UserInfo userInfo) { + MoEngageSdkUtils.trackUserLogin(this, userInfo); + } + private void endSuccess() { if (!onlyAdd) { if (MDMConfig.INSTANCE.enforceProtection(this) && Objects.equals(preferences.getLockPreference(), SettingsActivity.LOCK_NONE)) { diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index f006888799f0..588a6f9aa13a 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -15,6 +15,7 @@ import android.util.Pair; import com.nextcloud.client.account.User; +import com.nmc.android.marketTracking.MoEngageSdkUtils; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -533,6 +534,9 @@ private void saveFolderInDB() { newDir.setPermissions(createdRemoteFolder.getPermissions()); getStorageManager().saveFile(newDir); + // NMC: track create folder event + MoEngageSdkUtils.trackCreateFolderEvent(context, newDir); + Log_OC.d(TAG, "Create directory " + remotePath + " in Database"); } } diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index 8d32565b9c18..d7b67367f764 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -24,6 +24,7 @@ import com.nextcloud.client.network.Connectivity; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.utils.autoRename.AutoRename; +import com.nmc.android.marketTracking.MoEngageSdkUtils; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -1661,6 +1662,10 @@ private void updateOCFile(OCFile file, RemoteFile remoteFile) { file.setRemoteId(remoteFile.getRemoteId()); file.setPermissions(remoteFile.getPermissions()); file.setUploadTimestamp(remoteFile.getUploadTimestamp()); + + // NMC: track upload file event + // mOriginalStoragePath will help in deciding if Uploading file is from Scan or not + MoEngageSdkUtils.trackUploadFileEvent(mContext, file, mOriginalStoragePath); } public interface OnRenameListener { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index 1b8edf686845..6788d773f588 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java @@ -65,6 +65,7 @@ import com.nextcloud.utils.extensions.ViewExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; +import com.nmc.android.marketTracking.MoEngageSdkUtils; import com.owncloud.android.R; import com.owncloud.android.authentication.PassCodeManager; import com.owncloud.android.datamodel.ArbitraryDataProvider; @@ -594,13 +595,19 @@ private void onNavigationItemClicked(final MenuItem menuItem) { resetOnlyPersonalAndOnDevice(); setupToolbar(); handleSearchEvents(new SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH), menuItem.getItemId()); + // NMC: track fav screen event + MoEngageSdkUtils.trackFavouriteScreenEvent(this); } else if (itemId == R.id.nav_gallery) { resetOnlyPersonalAndOnDevice(); setupToolbar(); startPhotoSearch(menuItem.getItemId()); + // NMC: track media screen event + MoEngageSdkUtils.trackMediaScreenEvent(this); } else if (itemId == R.id.nav_on_device) { EventBus.getDefault().post(new ChangeMenuEvent()); showFiles(true, false); + // NMC: track offline files screen event + MoEngageSdkUtils.trackOfflineFilesScreenEvent(this); } else if (itemId == R.id.nav_uploads) { resetOnlyPersonalAndOnDevice(); startActivity(UploadListActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); @@ -635,6 +642,8 @@ private void onNavigationItemClicked(final MenuItem menuItem) { } else if (itemId == R.id.nav_shared) { resetOnlyPersonalAndOnDevice(); startSharedSearch(menuItem); + // NMC: track shared screen event + MoEngageSdkUtils.trackSharedScreenEvent(this); } else if (itemId == R.id.nav_recently_modified) { resetOnlyPersonalAndOnDevice(); startRecentlyModifiedSearch(menuItem); @@ -1051,6 +1060,9 @@ private void getAndDisplayUserQuota() { showQuota(false); } }); + + // NMC: track quota storage + MoEngageSdkUtils.trackQuotaStorage(this, quota); } } }); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 1c2157879a50..5569524473c4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -82,6 +82,7 @@ import com.nextcloud.utils.extensions.lastFragment import com.nextcloud.utils.extensions.logFileSize import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFolderPath import com.nextcloud.utils.view.FastScrollUtils +import com.nmc.android.marketTracking.MoEngageSdkUtils import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.databinding.FilesBinding @@ -270,6 +271,15 @@ class FileDisplayActivity : initSyncBroadcastReceiver() observeWorkerState() registerRefreshFolderEventReceiver() + + // NMC: handle custom action callback for notifications + MoEngageSdkUtils.handleCustomActionCallback(object : MoEngageSdkUtils.OnHandleCustomActionCallback { + override fun handleAction(actionType: MoEngageSdkUtils.CustomActionType) { + if (actionType == MoEngageSdkUtils.CustomActionType.RATING) { + inAppReviewHelper.performNativeReview(this@FileDisplayActivity) + } + } + }) } private fun loadSavedInstanceState(savedInstanceState: Bundle?) { @@ -357,6 +367,9 @@ class FileDisplayActivity : } } } + + // NMC: Notify MoEngage about Config Changes for In-App Notifications + MoEngageSdkUtils.handleConfigChangesForInAppNotification() } override fun onPostCreate(savedInstanceState: Bundle?) { @@ -448,10 +461,14 @@ class FileDisplayActivity : override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { when (requestCode) { // handle notification permission on API level >= 33 - PermissionUtil.PERMISSIONS_POST_NOTIFICATIONS -> + PermissionUtil.PERMISSIONS_POST_NOTIFICATIONS -> { // dialogue was dismissed -> prompt for storage permissions requestExternalStoragePermission(this, viewThemeUtils) + // NMC: Notify MoEngage about the post notification permission response + MoEngageSdkUtils.updatePostNotificationsPermission(this) + } + // If request is cancelled, result arrays are empty. PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -2813,6 +2830,9 @@ class FileDisplayActivity : preferences.lastDisplayedAccountName = newLastDisplayedAccountName lastDisplayedAccountName = newLastDisplayedAccountName + // NMC: show in-app notifications + MoEngageSdkUtils.displayInAppNotification(this) + EventBus.getDefault().post(TokenPushEvent()) checkForNewDevVersionNecessary(applicationContext) } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt index dc24fb140963..2d0241334ad0 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt @@ -31,6 +31,7 @@ import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.common.NextcloudClient import com.nextcloud.utils.BuildHelper.isFlavourGPlay import com.owncloud.android.R +import com.nmc.android.marketTracking.MoEngageSdkUtils import com.owncloud.android.databinding.NotificationsLayoutBinding import com.owncloud.android.datamodel.ArbitraryDataProvider import com.owncloud.android.datamodel.ArbitraryDataProviderImpl @@ -93,6 +94,9 @@ class NotificationsActivity : if (optionalUser?.isPresent == false) { showError() } + + // NMC: track notification screen event + MoEngageSdkUtils.trackNotificationsScreenEvent(this) } private fun initUser() { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index ca272e960aa4..cfb79f24786a 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -37,6 +37,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.preferences.SubFolderRule import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.isDialogFragmentReady +import com.nmc.android.marketTracking.MoEngageSdkUtils import com.owncloud.android.BuildConfig import com.owncloud.android.MainApp import com.owncloud.android.R @@ -712,6 +713,7 @@ class SyncedFoldersActivity : if (syncedFolder.isEnabled) { showBatteryOptimizationInfo() } + trackAutoUpload() } override fun showSubFolderWarningDialog() { @@ -774,6 +776,15 @@ class SyncedFoldersActivity : syncedFolderProvider.deleteSyncedFolder(syncedFolder.id) adapter.removeItem(syncedFolder.section) + trackAutoUpload() + } + + /** + * NMC: tracking auto upload is enabled or not + * Should be called whenever a Folder is saved or removed from auto upload + */ + private fun trackAutoUpload() { + MoEngageSdkUtils.trackAutoUpload(this, syncedFolderProvider.countEnabledSyncedFolders()) } /** diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt index dedef04d701e..e937e3d9fc4b 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt @@ -29,6 +29,7 @@ import com.nextcloud.client.network.ClientFactory.CreationException import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.fileNameValidator.FileNameValidator import com.owncloud.android.MainApp +import com.nmc.android.marketTracking.MoEngageSdkUtils import com.owncloud.android.R import com.owncloud.android.databinding.ChooseTemplateBinding import com.owncloud.android.datamodel.FileDataStorageManager @@ -399,6 +400,12 @@ class ChooseRichDocumentsTemplateDialogFragment : putExtra(ExternalSiteWebView.EXTRA_TEMPLATE, template) } + // NMC: track create office file event & open event + file?.let { + MoEngageSdkUtils.trackCreateFileEvent(MainApp.getAppContext(), it, template.type) + MoEngageSdkUtils.trackOnlineOfficeUsedEvent(MainApp.getAppContext(), it) + } + fragment.run { startActivity(intent) dismiss() diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt index a9f4e32e0f68..992db2081b66 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt @@ -33,6 +33,7 @@ import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.network.ClientFactory.CreationException import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.fileNameValidator.FileNameValidator +import com.nmc.android.marketTracking.MoEngageSdkUtils import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.databinding.ChooseTemplateBinding @@ -356,6 +357,11 @@ class ChooseTemplateDialogFragment : putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) } + // NMC: track create text file event + file?.let { + MoEngageSdkUtils.trackCreateFileEvent(MainApp.getAppContext(), it) + } + fragment.run { startActivity(editorWebView) dismiss() diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 8158c1a0c112..5233bef41bcb 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -63,6 +63,7 @@ import com.nextcloud.utils.fileNameValidator.FileNameValidator; import com.nextcloud.utils.view.FastScrollUtils; import com.owncloud.android.MainApp; +import com.nmc.android.marketTracking.MoEngageSdkUtils; import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -101,6 +102,7 @@ import com.owncloud.android.ui.events.FavoriteEvent; import com.owncloud.android.ui.events.FileLockEvent; import com.owncloud.android.ui.events.SearchEvent; +import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment; import com.owncloud.android.ui.helpers.FileOperationsHelper; import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface; import com.owncloud.android.ui.preview.PreviewImageFragment; @@ -340,9 +342,17 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, } Log_OC.i(TAG, "onCreateView() end"); + // NMC: track few user attributes at app launch + trackUserAttributes(); return v; } + private void trackUserAttributes() { + MoEngageSdkUtils.trackAutoUpload(requireContext(), syncedFolderProvider.countEnabledSyncedFolders()); + MoEngageSdkUtils.trackContactBackup(requireContext(), arbitraryDataProvider.getBooleanValue(accountManager.getUser(), + BackupFragment.PREFERENCE_CONTACTS_BACKUP_ENABLED)); + } + @Override public void onDetach() { setOnRefreshListener(null); @@ -540,6 +550,9 @@ public void registerFabListener() { dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); dialog.getBehavior().setSkipCollapsed(true); dialog.show(); + + // NMC: track action button item click event + MoEngageSdkUtils.trackActionButtonEvent(requireContext()); }); } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt index 57212ecb126e..b9439f013613 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt @@ -28,6 +28,7 @@ import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.utils.extensions.getSerializableArgument import com.nextcloud.utils.extensions.setVisibleIf +import com.nmc.android.marketTracking.MoEngageSdkUtils import com.owncloud.android.R import com.owncloud.android.databinding.BackupFragmentBinding import com.owncloud.android.datamodel.ArbitraryDataProvider @@ -99,6 +100,8 @@ class BackupFragment : PREFERENCE_CONTACTS_BACKUP_ENABLED, enabled ) + // NMC: track contact backup + MoEngageSdkUtils.trackContactBackup(requireContext(), enabled) } private lateinit var contactsBackupFolderPath: String diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index ca4679b4f41e..e98dffdc69d6 100755 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -36,6 +36,7 @@ import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.utils.EditorUtils; +import com.nmc.android.marketTracking.MoEngageSdkUtils; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; @@ -358,6 +359,9 @@ public void openFileAsRichDocument(OCFile file, Context context) { collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_FILE, file); collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false); context.startActivity(collaboraWebViewIntent); + + // NMC: track when office file opened event + MoEngageSdkUtils.trackOnlineOfficeUsedEvent(context, file); } public void openFileWithTextEditor(OCFile file, Context context) { @@ -449,6 +453,9 @@ public void shareFileViaPublicShare(OCFile file, String password) { service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); + // NMC: track link share event + MoEngageSdkUtils.trackShareFileEvent(fileActivity, file); + } else { Log_OC.e(TAG, "Trying to share a NULL OCFile"); // TODO user-level error? @@ -876,6 +883,9 @@ public void syncFile(OCFile file) { } else { Intent intent = getSyncFileIntent(file); mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent); + + // NMC: track offline available event + MoEngageSdkUtils.trackOfflineAvailableEvent(fileActivity, file); } } @@ -905,6 +915,9 @@ public void syncFile(OCFile file, boolean postDialogEvent) { Intent intent = getSyncFileIntent(file); intent.putExtra(OperationsService.EXTRA_POST_DIALOG_EVENT, postDialogEvent); mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent); + + // NMC: track offline available event + MoEngageSdkUtils.trackOfflineAvailableEvent(fileActivity, file); } } @@ -924,6 +937,11 @@ public void toggleFavoriteFiles(Collection files, boolean shouldBeFavori public void toggleFavoriteFile(OCFile file, boolean shouldBeFavorite) { if (file.isFavorite() != shouldBeFavorite) { EventBus.getDefault().post(new FavoriteEvent(file.getRemotePath(), shouldBeFavorite)); + + // NMC: capture whenever a file is added to favourite + if (shouldBeFavorite) { + MoEngageSdkUtils.trackAddFavoriteEvent(fileActivity, file); + } } } diff --git a/app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinActivity.kt b/app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinActivity.kt index 64a103bce539..eec5b949a00b 100644 --- a/app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinActivity.kt @@ -36,6 +36,7 @@ import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.client.utils.Throttler import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet import com.owncloud.android.R +import com.nmc.android.marketTracking.MoEngageSdkUtils import com.owncloud.android.databinding.TrashbinActivityBinding import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile @@ -135,6 +136,9 @@ class TrashbinActivity : active = true setupContent() + + // NMC: track deleted files screen event + MoEngageSdkUtils.trackDeletedFilesScreenEvent(this) } private fun setupContent() { diff --git a/app/src/main/res/xml/backup_config.xml b/app/src/main/res/xml/backup_config.xml index 37c85870abb1..cfb62ef06bc6 100644 --- a/app/src/main/res/xml/backup_config.xml +++ b/app/src/main/res/xml/backup_config.xml @@ -11,4 +11,12 @@ + + + + diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml index 4345e4238ec9..c14a4f466d02 100644 --- a/app/src/main/res/xml/backup_rules.xml +++ b/app/src/main/res/xml/backup_rules.xml @@ -17,5 +17,23 @@ + + + + + + + + + + diff --git a/app/src/qa/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt b/app/src/qa/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt index 34224244fc09..ee6f937491a8 100644 --- a/app/src/qa/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt +++ b/app/src/qa/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt @@ -17,4 +17,8 @@ class InAppReviewHelperImpl(appPreferences: AppPreferences) : InAppReviewHelper override fun showInAppReview(activity: AppCompatActivity) { } + + override fun performNativeReview(activity: AppCompatActivity){ + + } } diff --git a/app/src/versionDev/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt b/app/src/versionDev/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt index 34224244fc09..ee6f937491a8 100644 --- a/app/src/versionDev/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt +++ b/app/src/versionDev/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt @@ -17,4 +17,8 @@ class InAppReviewHelperImpl(appPreferences: AppPreferences) : InAppReviewHelper override fun showInAppReview(activity: AppCompatActivity) { } + + override fun performNativeReview(activity: AppCompatActivity){ + + } } diff --git a/gradle.properties b/gradle.properties index 50cc48359122..17ebec1de803 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,7 @@ NC_TEST_SERVER_USERNAME=test NC_TEST_SERVER_PASSWORD=test android.useAndroidX=true android.nonTransitiveRClass=true +MOENGAGE_APP_ID="7KWWUKA6OKXGP8Q6DMCXLDX5" #android.debug.obsoleteApi=true diff --git a/nmc_moengage-dependencies.gradle b/nmc_moengage-dependencies.gradle new file mode 100644 index 000000000000..f595820b3f59 --- /dev/null +++ b/nmc_moengage-dependencies.gradle @@ -0,0 +1,21 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +android { + buildTypes.each { + it.buildConfigField "String", "MOENGAGE_APP_ID", "${MOENGAGE_APP_ID}" + } +} + +dependencies { + // core moengage features + implementation(moengage.core) + // optionally add this to use the Push Templates feature + implementation(moengage.richNotification) + // optionally add this to use the InApp feature + implementation(moengage.inapp) +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index f8c7dc28b281..470a0b15b539 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,3 +20,15 @@ include ":appscan" // substitute module('com.github.nextcloud:android-library') using project(':library') // broken on gradle 8.14.2, so use 8.13 if needed // } //} + +// NMC: adding moengage catalog +dependencyResolutionManagement { + repositories { + mavenCentral() + } + versionCatalogs { + create("moengage") { + from("com.moengage:android-dependency-catalog:4.2.0") + } + } +}