Skip to content

Commit

Permalink
Merge pull request #686 from adobe/staging
Browse files Browse the repository at this point in the history
Merge `staging` into `main` for Core v3.1.0
  • Loading branch information
prudrabhat authored Jun 20, 2024
2 parents d297aa5 + 449a92b commit ed8f87a
Show file tree
Hide file tree
Showing 15 changed files with 642 additions and 11 deletions.
6 changes: 6 additions & 0 deletions code/core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public final class com/adobe/marketing/mobile/EventSource {
public static final field CONSENT_PREFERENCE Ljava/lang/String;
public static final field CONTENT_COMPLETE Ljava/lang/String;
public static final field CREATE_TRACKER Ljava/lang/String;
public static final field DEBUG Ljava/lang/String;
public static final field ERROR_RESPONSE_CONTENT Ljava/lang/String;
public static final field NONE Ljava/lang/String;
public static final field OS Ljava/lang/String;
Expand Down Expand Up @@ -1088,6 +1089,11 @@ public class com/adobe/marketing/mobile/util/EventDataUtils {
public static fun immutableClone (Ljava/util/Map;)Ljava/util/Map;
}

public final class com/adobe/marketing/mobile/util/EventUtils {
public static final fun getDebugEventSource (Lcom/adobe/marketing/mobile/Event;)Ljava/lang/String;
public static final fun getDebugEventType (Lcom/adobe/marketing/mobile/Event;)Ljava/lang/String;
}

public final class com/adobe/marketing/mobile/util/JSONUtils {
public static fun isNullOrEmpty (Lorg/json/JSONArray;)Z
public static fun isNullOrEmpty (Lorg/json/JSONObject;)Z
Expand Down
1 change: 1 addition & 0 deletions code/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ dependencies {
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
//TODO: Consider moving this to the aep-library plugin later
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.3")
testImplementation("org.robolectric:robolectric:4.7")
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ private EventSource() {}
public static final String CREATE_TRACKER = "com.adobe.eventSource.createTracker";
public static final String TRACK_MEDIA = "com.adobe.eventSource.trackMedia";
public static final String CONTENT_COMPLETE = "com.adobe.eventSource.contentComplete";
public static final String DEBUG = "com.adobe.eventSource.debug";
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ package com.adobe.marketing.mobile.internal

internal object CoreConstants {
const val LOG_TAG = "MobileCore"
const val VERSION = "3.0.2"
const val VERSION = "3.1.0"

object EventDataKeys {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
Copyright 2024 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

@file:JvmName("EventUtils") // Allows Java callers to use EventUtils.<> instead of EventUtilsKt.<>

package com.adobe.marketing.mobile.util

import com.adobe.marketing.mobile.Event
import com.adobe.marketing.mobile.EventSource
import com.adobe.marketing.mobile.EventType

private const val KEY_EVENT_DATA_DEBUG = "debug"
private const val KEY_DEBUG_EVENT_TYPE = "eventType"
private const val KEY_DEBUG_EVENT_SOURCE = "eventSource"

/**
* The debug event type (identified by debug.eventType) from the event data if present, otherwise null
*/
val Event.debugEventType: String?
get() = debugEventData?.get(KEY_DEBUG_EVENT_TYPE) as? String

/**
* The debug event source (identified by debug.eventSource) from the event data if present, otherwise null.
*/
val Event.debugEventSource: String?
get() = debugEventData?.get(KEY_DEBUG_EVENT_SOURCE) as? String

/**
* Returns the debug event data (identified by data.debug) from the event if present, otherwise null.
* @return the content of "debug" key within "Event.data" if present,
* null if the event is not a debug event or if the debug data does not exist
*/
private val Event.debugEventData: Map<String, Any?>?
get() {
if (type != EventType.SYSTEM || source != EventSource.DEBUG) return null

if (eventData == null) return null

return DataReader.optTypedMap(Any::class.java, eventData, KEY_EVENT_DATA_DEBUG, null) ?: null
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@

import android.app.Activity;
import android.app.Application;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.os.UserManagerCompat;
import com.adobe.marketing.mobile.internal.AppResourceStore;
import com.adobe.marketing.mobile.internal.CoreConstants;
import com.adobe.marketing.mobile.internal.DataMarshaller;
Expand Down Expand Up @@ -82,31 +86,49 @@ public static void setWrapperType(@NonNull final WrapperType wrapperType) {

/**
* Set the Android {@link Application}, which enables the SDK get the app {@code Context},
* register a {@link Application.ActivityLifecycleCallbacks} to monitor the lifecycle of the app
* and get the {@link android.app.Activity} on top of the screen.
* register a {@link ActivityLifecycleCallbacks} to monitor the lifecycle of the app and get the
* {@link Activity} on top of the screen.
*
* <p>NOTE: This method should be called right after the app starts, so it gives the SDK all the
* contexts it needed.
*
* @param application the Android {@link Application} instance. It should not be null.
*/
public static void setApplication(@NonNull final Application application) {

if (application == null) {
Log.error(
CoreConstants.LOG_TAG, LOG_TAG, "setApplication failed - application is null");
return;
}

// Direct boot mode is supported on Android N and above
if (VERSION.SDK_INT >= VERSION_CODES.N) {
if (UserManagerCompat.isUserUnlocked(application)) {
Log.debug(
CoreConstants.LOG_TAG,
LOG_TAG,
"setApplication - device is unlocked and not in direct boot mode,"
+ " initializing the SDK.");
} else {
Log.error(
CoreConstants.LOG_TAG,
LOG_TAG,
"setApplication failed - device is in direct boot mode, SDK will not be"
+ " initialized.");
return;
}
}

if (sdkInitializedWithContext.getAndSet(true)) {
Log.debug(
CoreConstants.LOG_TAG,
LOG_TAG,
"Ignoring as setApplication was already called.");
"setApplication failed - ignoring as setApplication was already called.");
return;
}

if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O
|| android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.O_MR1) {
if (VERSION.SDK_INT == VERSION_CODES.O || VERSION.SDK_INT == VERSION_CODES.O_MR1) {
// AMSDK-8502
// Workaround to prevent a crash happening on Android 8.0/8.1 related to
// TimeZoneNamesImpl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ internal abstract class AEPPresentable<T : Presentation<T>> :

@VisibleForTesting internal val contentIdentifier: Int = Random().nextInt()

/**
* Represents the activity to which the presentable is currently attached.
* This SHOULD always be updated whenever the presentable is attached or detached from an
* activity. For the sake of maintainability, modify this only in [attach] and [detach] and
* limit queries to this field in activity lifecycle methods.
*/
private var attachmentHandle: WeakReference<Activity?> = WeakReference(null)

/**
* @param presentation the [Presentation] to be used by this [Presentable]
* @param presentationUtilityProvider the [PresentationUtilityProvider] to be used to fetch components needed by this [Presentable]
Expand Down Expand Up @@ -241,6 +249,22 @@ internal abstract class AEPPresentable<T : Presentation<T>> :
if (getState() != Presentable.State.VISIBLE) {
return@launch
}

// If the activity that has currently resumed is not the same as the one the presentable
// is attached to, then it means that another activity has launched on top of/beside
// current activity. Detach the presentable from the current activity before attaching
// it to the newly resumed activity.
val currentAttachmentHandle = attachmentHandle.get()
if (currentAttachmentHandle != null && currentAttachmentHandle != activity) {
Log.trace(
ServiceConstants.LOG_TAG,
LOG_SOURCE,
"Detaching from $currentAttachmentHandle before attaching to $activity."
)

detach(currentAttachmentHandle)
}

attach(activity)
}
}
Expand Down Expand Up @@ -327,6 +351,10 @@ internal abstract class AEPPresentable<T : Presentation<T>> :
composeView.id = contentIdentifier
val rootViewGroup = activityToAttach.findViewById<ViewGroup>(android.R.id.content)
rootViewGroup.addView(composeView)

// Update the attachment handle to the currently attached activity.
attachmentHandle = WeakReference(activityToAttach)

Log.trace(
ServiceConstants.LOG_TAG,
LOG_SOURCE,
Expand Down Expand Up @@ -373,6 +401,20 @@ internal abstract class AEPPresentable<T : Presentation<T>> :
}
existingComposeView.removeAllViews()
rootViewGroup.removeView(existingComposeView)

// Clear the attachment handle if the current attachment handle is the same as the activity
// to detach. If not, the handle would have already been cleared when the presentable
// was attached due to another activity being resumed on top of the presentable.
val currentAttachmentHandle = attachmentHandle.get()
if (currentAttachmentHandle == activityToDetach) {
Log.trace(
ServiceConstants.LOG_TAG,
LOG_SOURCE,
"Clearing attachment handle ($activityToDetach)."
)
attachmentHandle.clear()
}

activityCompatOwnerUtils.detachActivityCompatOwner(activityToDetach)
Log.trace(ServiceConstants.LOG_TAG, LOG_SOURCE, "Detached ${contentIdentifier}from $activityToDetach.")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ internal fun MessageFrame(
// the WebView message is clipped to the rounded corners for API versions 22 and below. This does not
// affect the appearance of the message on API versions 23 and above.
Card(
backgroundColor = Color.Transparent,
modifier = Modifier
.clip(RoundedCornerShape(inAppMessageSettings.cornerRadius.dp))
.alpha(0.99f)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
Copyright 2024 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

package com.adobe.marketing.mobile

import android.app.Application
import androidx.core.os.UserManagerCompat
import com.adobe.marketing.mobile.internal.eventhub.EventHub
import junit.framework.TestCase.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.assertFalse

@RunWith(RobolectricTestRunner::class)
class MobileCoreRobolectricTests {

@Before
fun setup() {
MobileCore.sdkInitializedWithContext = AtomicBoolean(false)
}

@Test
@Config(sdk = [23])
fun `test setApplication when the device doesn't support direct boot mode`() {
// Android supports direct boot mode on API level 24 and above
val app = RuntimeEnvironment.application as Application
val mockedEventHub = Mockito.mock(EventHub::class.java)
Mockito.mockStatic(UserManagerCompat::class.java).use { mockedStaticUserManagerCompat ->
mockedStaticUserManagerCompat.`when`<Any> { UserManagerCompat.isUserUnlocked(Mockito.any()) }
.thenReturn(false)
MobileCore.setApplication(app)
mockedStaticUserManagerCompat.verify({ UserManagerCompat.isUserUnlocked(Mockito.any()) }, never())
}
verify(mockedEventHub, never()).executeInEventHubExecutor(any())
assertTrue(MobileCore.sdkInitializedWithContext.get())
}

@Test
@Config(sdk = [24])
fun `test setApplication when the app is not configured to run in direct boot mode`() {
val app = RuntimeEnvironment.application as Application
val mockedEventHub = Mockito.mock(EventHub::class.java)
EventHub.shared = mockedEventHub
Mockito.mockStatic(UserManagerCompat::class.java).use { mockedStaticUserManagerCompat ->
// when initializing SDK, the app is not in direct boot mode (device is unlocked)
mockedStaticUserManagerCompat.`when`<Any> { UserManagerCompat.isUserUnlocked(Mockito.any()) }.thenReturn(true)
MobileCore.setApplication(app)
mockedStaticUserManagerCompat.verify({ UserManagerCompat.isUserUnlocked(Mockito.any()) }, times(1))
}
verify(mockedEventHub, times(1)).executeInEventHubExecutor(any())
assertTrue(MobileCore.sdkInitializedWithContext.get())
}

@Test
@Config(sdk = [24])
fun `test setApplication when the app is launched in direct boot mode`() {
val app = RuntimeEnvironment.application as Application
val mockedEventHub = Mockito.mock(EventHub::class.java)
Mockito.mockStatic(UserManagerCompat::class.java).use { mockedStaticUserManagerCompat ->
// when initializing SDK, the app is in direct boot mode (device is still locked)
mockedStaticUserManagerCompat.`when`<Any> { UserManagerCompat.isUserUnlocked(Mockito.any()) }.thenReturn(false)
MobileCore.setApplication(app)
mockedStaticUserManagerCompat.verify({ UserManagerCompat.isUserUnlocked(Mockito.any()) }, times(1))
}
verify(mockedEventHub, never()).executeInEventHubExecutor(any())
assertFalse(MobileCore.sdkInitializedWithContext.get())
}
}
Loading

0 comments on commit ed8f87a

Please sign in to comment.