From 63e29e0f3c127914a91aa59925aab36c534eefa5 Mon Sep 17 00:00:00 2001 From: A117870935 Date: Sun, 9 Jun 2024 23:12:28 +0530 Subject: [PATCH 1/6] MoEngage SDK setup --- app/build.gradle | 13 +++++++ .../marketTracking/MoEngageSdkUtils.kt | 35 +++++++++++++++++++ .../java/com/owncloud/android/MainApp.java | 3 ++ app/src/main/res/xml/backup_config.xml | 8 +++++ app/src/main/res/xml/backup_rules.xml | 18 ++++++++++ gradle.properties | 1 + settings.gradle | 12 +++++++ 7 files changed, 90 insertions(+) create mode 100644 app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt diff --git a/app/build.gradle b/app/build.gradle index cfa9e535897b..578012ebc294 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,6 +109,8 @@ android { targetSdkVersion 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() @@ -428,6 +430,17 @@ dependencies { implementation "io.coil-kt:coil:2.7.0" + // NMC: MoEngage 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) + // Required for MoEngage to provide accurate analytics based on Advertising Identifier + implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1' + // NMC: end + // splash screen dependency ref: https://developer.android.com/develop/ui/views/launch/splash-screen/migrate implementation 'androidx.core:core-splashscreen:1.0.1' } 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..7f48de133e9f --- /dev/null +++ b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nmc.android.marketTracking + +import android.app.Application +import android.content.Context +import com.moengage.core.DataCenter +import com.moengage.core.MoEngage +import com.moengage.core.enableAdIdTracking +import com.moengage.core.enableAndroidIdTracking +import com.owncloud.android.BuildConfig + +object MoEngageSdkUtils { + + @JvmStatic + fun initMoEngageSDK(application: Application) { + val moEngage = MoEngage.Builder(application, BuildConfig.MOENGAGE_APP_ID, DataCenter.DATA_CENTER_2) + .build() + MoEngage.initialiseDefaultInstance(moEngage) + enableDeviceIdentifierTracking(application) + } + + // for NMC the default privacy tracking consent is always taken from users + // so the tracking will always be enabled for MoEngage + @JvmStatic + internal fun enableDeviceIdentifierTracking(context: Context) { + enableAndroidIdTracking(context) + enableAdIdTracking(context) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 95ffbcd0a614..85f87baca902 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; @@ -313,6 +314,8 @@ public void onCreate() { registerActivityLifecycleCallbacks(new ActivityInjector()); + // NMC: init MoEngage SDK + MoEngageSdkUtils.initMoEngageSDK(this); //update the app restart count when app is launched by the user inAppReviewHelper.resetAndIncrementAppRestartCounter(); 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/gradle.properties b/gradle.properties index 14f2cb920c7e..36b6a99a4585 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,7 @@ android.enableJetifier=true android.useAndroidX=true android.nonTransitiveRClass=false android.nonFinalResIds=false +MOENGAGE_APP_ID="7KWWUKA6OKXGP8Q6DMCXLDX5" #android.debug.obsoleteApi=true diff --git a/settings.gradle b/settings.gradle index 352229bd93d9..e2333193f276 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,3 +20,15 @@ include ':appscan' // substitute module('com.github.nextcloud:android-library') using project(':library') // } //} + +// NMC: adding moengage catalog +dependencyResolutionManagement { + repositories { + mavenCentral() + } + versionCatalogs { + create("moengage") { + from("com.moengage:android-dependency-catalog:4.2.0") + } + } +} From 41c2e3d3333bdb1f7853deb553e6ca4f1fc89d43 Mon Sep 17 00:00:00 2001 From: A117870935 Date: Tue, 11 Jun 2024 19:38:03 +0530 Subject: [PATCH 2/6] Logic added for tracking app install and update. --- .../marketTracking/MoEngageSdkUtils.kt | 22 +++++++++++++++++++ .../java/com/owncloud/android/MainApp.java | 2 ++ 2 files changed, 24 insertions(+) diff --git a/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt index 7f48de133e9f..c8f2ce94dd47 100644 --- a/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt +++ b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt @@ -11,8 +11,10 @@ import android.app.Application import android.content.Context import com.moengage.core.DataCenter import com.moengage.core.MoEngage +import com.moengage.core.analytics.MoEAnalyticsHelper import com.moengage.core.enableAdIdTracking import com.moengage.core.enableAndroidIdTracking +import com.moengage.core.model.AppStatus import com.owncloud.android.BuildConfig object MoEngageSdkUtils { @@ -32,4 +34,24 @@ object MoEngageSdkUtils { enableAndroidIdTracking(context) enableAdIdTracking(context) } + + @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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 85f87baca902..c90893e8ad5c 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -316,6 +316,8 @@ public void onCreate() { // NMC: init MoEngage SDK MoEngageSdkUtils.initMoEngageSDK(this); + MoEngageSdkUtils.trackAppInstallOrUpdate(this, preferences.getLastSeenVersionCode()); + // NMC: end //update the app restart count when app is launched by the user inAppReviewHelper.resetAndIncrementAppRestartCounter(); From b29c713ed33fba90335b346765adbbb14ed5167f Mon Sep 17 00:00:00 2001 From: A117870935 Date: Wed, 12 Jun 2024 22:33:57 +0530 Subject: [PATCH 3/6] Added User Attributes tracking. --- .../client/jobs/AccountRemovalWork.kt | 3 + .../marketTracking/MoEngageSdkUtils.kt | 71 ++++++++++++++++++- .../authentication/AuthenticatorActivity.java | 7 ++ .../android/ui/activity/DrawerActivity.java | 4 ++ .../ui/activity/SyncedFoldersActivity.kt | 11 +++ .../ui/fragment/OCFileListFragment.java | 10 +++ .../fragment/contactsbackup/BackupFragment.kt | 3 + 7 files changed, 107 insertions(+), 2 deletions(-) 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/nmc/android/marketTracking/MoEngageSdkUtils.kt b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt index c8f2ce94dd47..4fe3af7f7941 100644 --- a/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt +++ b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt @@ -10,31 +10,48 @@ package com.nmc.android.marketTracking import android.app.Application import android.content.Context import com.moengage.core.DataCenter +import com.moengage.core.MoECoreHelper import com.moengage.core.MoEngage import com.moengage.core.analytics.MoEAnalyticsHelper import com.moengage.core.enableAdIdTracking import com.moengage.core.enableAndroidIdTracking import com.moengage.core.model.AppStatus import com.owncloud.android.BuildConfig +import com.owncloud.android.lib.common.Quota +import com.owncloud.android.lib.common.UserInfo +import com.owncloud.android.utils.DisplayUtils +import kotlin.math.ceil 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" + @JvmStatic fun initMoEngageSDK(application: Application) { val moEngage = MoEngage.Builder(application, BuildConfig.MOENGAGE_APP_ID, DataCenter.DATA_CENTER_2) .build() MoEngage.initialiseDefaultInstance(moEngage) 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 - @JvmStatic - internal fun enableDeviceIdentifierTracking(context: Context) { + private fun enableDeviceIdentifierTracking(context: Context) { enableAndroidIdTracking(context) enableAdIdTracking(context) } + private fun trackAppVersion(context: Context) { + MoEAnalyticsHelper.setUserAttribute(context, USER_PROPERTIES__APP_VERSION, BuildConfig.VERSION_NAME) + } + @JvmStatic fun trackAppInstallOrUpdate(context: Context, lastSeenVersionCode: Int) { if (lastSeenVersionCode <= 0) { @@ -54,4 +71,54 @@ object MoEngageSdkUtils { // 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) { + DisplayUtils.bytesToHumanReadable(it.total) + } 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 trackUserLogout(context: Context) { + MoECoreHelper.logoutUser(context) + } } \ No newline at end of file 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 d3c33f5d45db..39f156e98bad 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -64,6 +64,7 @@ import com.nextcloud.utils.extensions.BundleExtensionsKt; 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.databinding.AccountSetupBinding; import com.owncloud.android.databinding.AccountSetupWebviewBinding; @@ -1143,6 +1144,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()); @@ -1187,6 +1190,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/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index 3e02c21624a7..79a777cb3caa 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 @@ -63,6 +63,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; @@ -1020,6 +1021,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/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index d18c26825cb6..c9d1e312e9e7 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 @@ -35,6 +35,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 @@ -709,6 +710,7 @@ class SyncedFoldersActivity : if (syncedFolder.isEnabled) { showBatteryOptimizationInfo() } + trackAutoUpload() } private fun saveOrUpdateSyncedFolder(item: SyncedFolderDisplayItem) { @@ -755,6 +757,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/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 556cbbcc6637..0aaaf815c15b 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 @@ -65,6 +65,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; @@ -105,6 +106,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; @@ -343,9 +345,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); 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 52e7d03b695b..532b36c309cb 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.account.User import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.utils.extensions.getSerializableArgument +import com.nmc.android.marketTracking.MoEngageSdkUtils import com.owncloud.android.R import com.owncloud.android.databinding.BackupFragmentBinding import com.owncloud.android.datamodel.ArbitraryDataProvider @@ -93,6 +94,8 @@ class BackupFragment : FileFragment(), OnDateSetListener, Injectable { PREFERENCE_CONTACTS_BACKUP_ENABLED, enabled ) + // NMC: track contact backup + MoEngageSdkUtils.trackContactBackup(requireContext(), enabled) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { From c5cd419ea4cce245c5734208a47b5e703755521b Mon Sep 17 00:00:00 2001 From: A117870935 Date: Wed, 19 Jun 2024 11:06:41 +0530 Subject: [PATCH 4/6] Logic added for tracking events. --- .../java/com/nextcloud/utils/ShortcutUtil.kt | 4 + .../MoEngagePropertiesHelper.kt | 26 ++ .../marketTracking/MoEngageSdkUtils.kt | 296 +++++++++++++++++- .../java/com/nmc/android/utils/FileUtils.java | 171 ++++++++++ .../java/com/owncloud/android/MainApp.java | 11 +- .../operations/CreateFolderOperation.java | 4 + .../operations/UploadFileOperation.java | 5 + .../android/ui/activity/DrawerActivity.java | 10 + .../ui/dialog/ChooseTemplateDialogFragment.kt | 6 + .../ui/fragment/OCFileListFragment.java | 3 + .../ui/helpers/FileOperationsHelper.java | 14 + 11 files changed, 544 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/nmc/android/marketTracking/MoEngagePropertiesHelper.kt create mode 100644 app/src/main/java/com/nmc/android/utils/FileUtils.java diff --git a/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt b/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt index 82fdbaab166b..d5d327e548be 100644 --- a/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt +++ b/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt @@ -21,6 +21,7 @@ import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.toBitmap 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 @@ -91,6 +92,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 index 4fe3af7f7941..ad66ccdc2fdc 100644 --- a/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt +++ b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt @@ -12,15 +12,27 @@ import android.content.Context 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.enableAdIdTracking import com.moengage.core.enableAndroidIdTracking import com.moengage.core.model.AppStatus +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.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.utils.DisplayUtils +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 kotlin.math.ceil +import kotlin.math.floor object MoEngageSdkUtils { @@ -30,6 +42,43 @@ object MoEngageSdkUtils { 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" + @JvmStatic fun initMoEngageSDK(application: Application) { val moEngage = MoEngage.Builder(application, BuildConfig.MOENGAGE_APP_ID, DataCenter.DATA_CENTER_2) @@ -52,6 +101,26 @@ object MoEngageSdkUtils { 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, + isUserLoggedIn: Boolean, + user: User + ) { + if (lastSeenVersionCode in 1..OLD_VERSION_CODE && isUserLoggedIn) { + 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) { @@ -77,10 +146,10 @@ object MoEngageSdkUtils { userInfo.id?.let { MoEAnalyticsHelper.setUniqueId(context, it) } - userInfo.displayName?.let{ + userInfo.displayName?.let { MoEAnalyticsHelper.setUserName(context, it) } - userInfo.email?.let{ + userInfo.email?.let { MoEAnalyticsHelper.setEmailId(context, it) } trackQuotaStorage(context, userInfo.quota) @@ -90,7 +159,7 @@ object MoEngageSdkUtils { fun trackQuotaStorage(context: Context, quota: Quota?) { quota?.let { val totalQuota = if (it.quota > 0) { - DisplayUtils.bytesToHumanReadable(it.total) + bytesToGB(it.total).toString() } else { it.total.toString() } @@ -116,9 +185,228 @@ object MoEngageSdkUtils { 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, bytesToMB(file.fileLength).toString()) + properties.addAttribute( + PROPERTIES__CREATION_DATE, + 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, + 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, bytesToMB(file.fileLength).toString()) + properties.addAttribute( + PROPERTIES__CREATION_DATE, + file.modificationTimestamp.getFormattedStringDate(DATE_FORMAT) + ) + properties.addAttribute(PROPERTIES__UPLOAD_DATE, file.modificationTimestamp.getFormattedStringDate(DATE_FORMAT)) + return properties + } + + private fun bytesToGB(bytes: Long): Int { + return floor((bytes / GIGABYTE).toDouble()).toInt() + } + + private fun bytesToMB(bytes: Long): Int { + return floor((bytes / MEGABYTE).toDouble()).toInt() + } + + 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 + } + + 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() + } } \ 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..a161a2acbdb9 --- /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")); + } + } + + private 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(".pdf", ".txt")); + } + } + + 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 c90893e8ad5c..3e541d83052a 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -315,8 +315,7 @@ public void onCreate() { registerActivityLifecycleCallbacks(new ActivityInjector()); // NMC: init MoEngage SDK - MoEngageSdkUtils.initMoEngageSDK(this); - MoEngageSdkUtils.trackAppInstallOrUpdate(this, preferences.getLastSeenVersionCode()); + initMoEngage(); // NMC: end //update the app restart count when app is launched by the user inAppReviewHelper.resetAndIncrementAppRestartCounter(); @@ -997,6 +996,14 @@ private static void cleanOldEntries(Clock clock) { } } + private void initMoEngage(){ + MoEngageSdkUtils.initMoEngageSDK(this); + MoEngageSdkUtils.trackAppInstallOrUpdate(this, preferences.getLastSeenVersionCode()); + MoEngageSdkUtils.captureUserAttrsForOldAppVersion(this, preferences.getLastSeenVersionCode(), + accountManager.getCurrentAccount() != null, + accountManager.getUser()); + } + @Override public AndroidInjector androidInjector() { return dispatchingAndroidInjector; 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 b2b922135caf..4f5155bff83a 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; @@ -1612,6 +1613,10 @@ private void updateOCFile(OCFile file, RemoteFile remoteFile) { file.setEtag(remoteFile.getEtag()); file.setRemoteId(remoteFile.getRemoteId()); file.setPermissions(remoteFile.getPermissions()); + + // 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 79a777cb3caa..9770f9b27ed3 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 @@ -587,15 +587,23 @@ private void onNavigationItemClicked(final MenuItem menuItem) { } else if (itemId == R.id.nav_favorites) { handleSearchEvents(new SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH), menuItem.getItemId()); + // NMC: track fav screen event + MoEngageSdkUtils.trackFavouriteScreenEvent(this); } else if (itemId == R.id.nav_gallery) { 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) { startActivity(UploadListActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); } else if (itemId == R.id.nav_trashbin) { startActivity(TrashbinActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); + // NMC: track deleted files screen event + MoEngageSdkUtils.trackDeletedFilesScreenEvent(this); } else if (itemId == R.id.nav_activity) { startActivity(ActivitiesActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); } else if (itemId == R.id.nav_settings) { @@ -612,6 +620,8 @@ private void onNavigationItemClicked(final MenuItem menuItem) { } } else if (itemId == R.id.nav_shared) { startSharedSearch(menuItem); + // NMC: track shared screen event + MoEngageSdkUtils.trackSharedScreenEvent(this); } else if (itemId == R.id.nav_recently_modified) { startRecentlyModifiedSearch(menuItem); } else if (itemId == R.id.nav_assistant) { 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 34740ce9604e..848018a59885 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 @@ -350,6 +351,11 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem 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 0aaaf815c15b..52d45b3952cd 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 @@ -549,6 +549,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/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index f6fa855dfd63..448e586a88a1 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; @@ -354,6 +355,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) { @@ -461,6 +465,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? @@ -877,6 +884,8 @@ public void syncFile(OCFile file) { fileActivity.showLoadingDialog(fileActivity.getApplicationContext(). getString(R.string.wait_a_moment)); + // NMC: track offline available event + MoEngageSdkUtils.trackOfflineAvailableEvent(fileActivity, file); } else { Intent intent = new Intent(fileActivity, OperationsService.class); intent.setAction(OperationsService.ACTION_SYNC_FOLDER); @@ -902,6 +911,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); + } } } From 681f412007347b7c96cde7c687bbb45ab2b13e3c Mon Sep 17 00:00:00 2001 From: A117870935 Date: Fri, 21 Jun 2024 15:05:35 +0530 Subject: [PATCH 5/6] Push and In-App Notification configuration. --- app/src/gplay/AndroidManifest.xml | 6 ++ .../firebase/NCFirebaseMessagingService.java | 8 +++ .../marketTracking/MoEngageSdkUtils.kt | 70 ++++++++++++++++--- .../ui/activity/FileDisplayActivity.java | 10 +++ 4 files changed, 86 insertions(+), 8 deletions(-) diff --git a/app/src/gplay/AndroidManifest.xml b/app/src/gplay/AndroidManifest.xml index 34de04e2556d..aea73fc2d74a 100644 --- a/app/src/gplay/AndroidManifest.xml +++ b/app/src/gplay/AndroidManifest.xml @@ -70,6 +70,12 @@ + + + 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/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt index ad66ccdc2fdc..39a308c4ddf4 100644 --- a/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt +++ b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt @@ -7,21 +7,27 @@ 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.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 @@ -31,6 +37,7 @@ 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 @@ -79,11 +86,23 @@ object MoEngageSdkUtils { private const val DATE_FORMAT = "yyyy-MM-dd" + // maximum post notification permission retry count + private const val PUSH_PERMISSION_REQUEST_RETRY_COUNT = 2 + @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 + ) + ) .build() MoEngage.initialiseDefaultInstance(moEngage) + + updatePostNotificationsPermission(application) + enableDeviceIdentifierTracking(application) // track app version at app launch @@ -191,7 +210,7 @@ object MoEngageSdkUtils { } @JvmStatic - fun trackUploadFileEvent(context: Context, file: OCFile, originalStoragePath : String) { + fun trackUploadFileEvent(context: Context, file: OCFile, originalStoragePath: String) { if (file.isFolder) return MoEAnalyticsHelper.trackEvent( @@ -384,7 +403,7 @@ object MoEngageSdkUtils { } } - private fun fetchUserInfo(context: Context, user : User) { + private fun fetchUserInfo(context: Context, user: User) { val t = Thread(Runnable { val nextcloudClient: NextcloudClient try { @@ -398,15 +417,50 @@ object MoEngageSdkUtils { } val result = GetUserInfoRemoteOperation().execute(nextcloudClient) - if (result.isSuccess && result.resultData != null) { - val userInfo = result.resultData + if (result.isSuccess && result.resultData != null) { + val userInfo = result.resultData - trackUserLogin(context, userInfo) - } else { - Log_OC.d(this, result.logMessage) - } + 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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index d802d36a8703..defd35cc75b0 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -73,6 +73,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.databinding.FilesBinding; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -455,6 +456,9 @@ public void onConfigurationChanged(@NonNull Configuration newConfig) { } } } + + // NMC: Notify MoEngage about Config Changes for In-App Notifications + MoEngageSdkUtils.handleConfigChangesForInAppNotification(); } @Override @@ -545,6 +549,9 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis // handle notification permission on API level >= 33 // dialogue was dismissed -> prompt for storage permissions PermissionUtil.requestExternalStoragePermission(this, viewThemeUtils); + + // NMC: Notify MoEngage about the post notification permission response + MoEngageSdkUtils.updatePostNotificationsPermission(this); break; case PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE: // If request is cancelled, result arrays are empty. @@ -2505,6 +2512,9 @@ public void onStart() { } lastDisplayedUser = optionalUser; + // NMC: show in-app notifications + MoEngageSdkUtils.displayInAppNotification(this); + EventBus.getDefault().post(new TokenPushEvent()); checkForNewDevVersionNecessary(getApplicationContext()); } From 6f25a6581fa695f4e28e204ee2da3817d3192b4f Mon Sep 17 00:00:00 2001 From: A117870935 Date: Thu, 27 Jun 2024 13:28:43 +0530 Subject: [PATCH 6/6] Dependency segregation --- app/build.gradle | 19 ++++---------- .../marketTracking/MoEngageSdkUtils.kt | 25 +++++++++++++------ .../java/com/nmc/android/utils/FileUtils.java | 4 +-- .../java/com/owncloud/android/MainApp.java | 1 - .../android/ui/activity/DrawerActivity.java | 2 -- .../ui/activity/NotificationsActivity.kt | 4 +++ ...ooseRichDocumentsTemplateDialogFragment.kt | 7 ++++++ .../android/ui/trashbin/TrashbinActivity.kt | 4 +++ nmc_moengage-dependencies.gradle | 21 ++++++++++++++++ 9 files changed, 60 insertions(+), 27 deletions(-) create mode 100644 nmc_moengage-dependencies.gradle diff --git a/app/build.gradle b/app/build.gradle index 578012ebc294..33a7f72fae06 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 @@ -109,8 +110,6 @@ android { targetSdkVersion 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() @@ -430,19 +429,11 @@ dependencies { implementation "io.coil-kt:coil:2.7.0" - // NMC: MoEngage 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) - // Required for MoEngage to provide accurate analytics based on Advertising Identifier - implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1' - // NMC: end - // splash screen dependency ref: https://developer.android.com/develop/ui/views/launch/splash-screen/migrate implementation 'androidx.core:core-splashscreen:1.0.1' + + // 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/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt index 39a308c4ddf4..c19ed45f456b 100644 --- a/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt +++ b/app/src/main/java/com/nmc/android/marketTracking/MoEngageSdkUtils.kt @@ -40,6 +40,7 @@ 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 { @@ -95,7 +96,9 @@ object MoEngageSdkUtils { .configureNotificationMetaData( NotificationConfig( R.drawable.notification_icon, - R.drawable.notification_icon + R.drawable.notification_icon, + R.color.primary, + false ) ) .build() @@ -129,10 +132,9 @@ object MoEngageSdkUtils { fun captureUserAttrsForOldAppVersion( context: Context, lastSeenVersionCode: Int, - isUserLoggedIn: Boolean, user: User ) { - if (lastSeenVersionCode in 1..OLD_VERSION_CODE && isUserLoggedIn) { + if (lastSeenVersionCode in 1..OLD_VERSION_CODE && !user.isAnonymous) { fetchUserInfo(context, user) } @@ -227,9 +229,10 @@ object MoEngageSdkUtils { val properties = Properties() properties.addAttribute(PROPERTIES__FILE_TYPE, getOfficeFileType(type) { getFileType(file) }.fileType) - properties.addAttribute(PROPERTIES__FILE_SIZE, bytesToMB(file.fileLength).toString()) + 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) ) @@ -244,6 +247,7 @@ object MoEngageSdkUtils { 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) ) @@ -323,12 +327,13 @@ object MoEngageSdkUtils { 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, bytesToMB(file.fileLength).toString()) + 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.modificationTimestamp.getFormattedStringDate(DATE_FORMAT)) + properties.addAttribute(PROPERTIES__UPLOAD_DATE, file.uploadTimestamp.getFormattedStringDate(DATE_FORMAT)) return properties } @@ -336,8 +341,9 @@ object MoEngageSdkUtils { return floor((bytes / GIGABYTE).toDouble()).toInt() } - private fun bytesToMB(bytes: Long): Int { - return floor((bytes / MEGABYTE).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 { @@ -414,6 +420,9 @@ object MoEngageSdkUtils { } 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) diff --git a/app/src/main/java/com/nmc/android/utils/FileUtils.java b/app/src/main/java/com/nmc/android/utils/FileUtils.java index a161a2acbdb9..97496b9a5f59 100644 --- a/app/src/main/java/com/nmc/android/utils/FileUtils.java +++ b/app/src/main/java/com/nmc/android/utils/FileUtils.java @@ -93,12 +93,12 @@ private static File getTextFileName(Context context, String fileName) { } } - private static File getPdfFileName(Context context, String fileName) { + 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(".pdf", ".txt")); + return new File(pdfFileName.getPath() + File.separator + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".pdf")); } } diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 3e541d83052a..70bb3a0e90ac 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -1000,7 +1000,6 @@ private void initMoEngage(){ MoEngageSdkUtils.initMoEngageSDK(this); MoEngageSdkUtils.trackAppInstallOrUpdate(this, preferences.getLastSeenVersionCode()); MoEngageSdkUtils.captureUserAttrsForOldAppVersion(this, preferences.getLastSeenVersionCode(), - accountManager.getCurrentAccount() != null, accountManager.getUser()); } 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 9770f9b27ed3..5f9ce22d9b1a 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 @@ -602,8 +602,6 @@ private void onNavigationItemClicked(final MenuItem menuItem) { startActivity(UploadListActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); } else if (itemId == R.id.nav_trashbin) { startActivity(TrashbinActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); - // NMC: track deleted files screen event - MoEngageSdkUtils.trackDeletedFilesScreenEvent(this); } else if (itemId == R.id.nav_activity) { startActivity(ActivitiesActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); } else if (itemId == R.id.nav_settings) { 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 ada5a2da54f1..193272b81054 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 @@ -29,6 +29,7 @@ import com.nextcloud.client.network.ClientFactory.CreationException import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.common.NextcloudClient 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 @@ -87,6 +88,9 @@ class NotificationsActivity : AppCompatActivity(), NotificationsContract.View, I 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/dialog/ChooseRichDocumentsTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt index b3b5eac4cfe4..033077c21aeb 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 @@ -401,6 +402,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/trashbin/TrashbinActivity.kt b/app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinActivity.kt index ebb94da68f90..df3d40910ca6 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 @@ -35,6 +35,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 @@ -131,6 +132,9 @@ class TrashbinActivity : active = true setupContent() + + // NMC: track deleted files screen event + MoEngageSdkUtils.trackDeletedFilesScreenEvent(this) } private fun setupContent() { 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