diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f4b4384..f98f1fe76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 26.1.2 +* Added `CountlyInitProvider` ContentProvider to register activity lifecycle callbacks before `Application.onCreate()`. This ensures the SDK captures the current activity in single-activity frameworks (Flutter, React Native) and apps with deferred initialization. +* Added `CountlyConfig.setInitialActivity(Activity)` as an explicit way for wrapper SDKs to provide the host activity during initialization. +* Added a new config option `setMetricProvider(MetricProvider)` to allow overriding default device metrics with custom values. + +## 26.1.1 +* Added Content feature method `previewContent(String contentId)` (Experimental!). +* Improved content display and refresh mechanics. + +* Mitigated an issue about health checks storage in explicit storage mode. + ## 26.1.0 * Extended server configuration capabilities with server-controlled listing filters: * Event filters (blacklist/whitelist) to control which events are recorded. diff --git a/README.md b/README.md index 46f7bdc8b..c48233a73 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Codacy Badge](https://app.codacy.com/project/badge/Grade/b26d1acc435c47af88b4e4b9eb94f59f)](https://app.codacy.com/gh/Countly/countly-sdk-android/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) -[![API](https://img.shields.io/badge/API-9%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=9) +![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat) # Countly Android SDK diff --git a/app-kotlin/build.gradle b/app-kotlin/build.gradle index 607dfabb3..4b5e6d0c7 100644 --- a/app-kotlin/build.gradle +++ b/app-kotlin/build.gradle @@ -25,14 +25,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } + dataBinding { enabled = true } diff --git a/app/build.gradle b/app/build.gradle index 3e21dc9c0..507064199 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,10 +28,19 @@ repositories { } } +kotlin { + jvmToolchain(17) +} + android { compileSdk 35 namespace 'ly.count.android.demo' + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + signingConfigs { release { storeFile file('keys') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6ca09f3f2..c12a5034f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ @@ -105,6 +106,11 @@ android:label="@string/activity_name_content_zone" android:configChanges="orientation|screenSize"/> + + @@ -149,6 +155,21 @@ + + + + + + diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleConsent.java b/app/src/main/java/ly/count/android/demo/ActivityExampleConsent.java new file mode 100644 index 000000000..e52731dcf --- /dev/null +++ b/app/src/main/java/ly/count/android/demo/ActivityExampleConsent.java @@ -0,0 +1,74 @@ +package ly.count.android.demo; + +import android.os.Bundle; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.switchmaterial.SwitchMaterial; + +import ly.count.android.sdk.Countly; + +public class ActivityExampleConsent extends AppCompatActivity { + + private static final String[][] FEATURES = { + {"sessions", "switchSessions"}, + {"events", "switchEvents"}, + {"views", "switchViews"}, + {"crashes", "switchCrashes"}, + {"attribution", "switchAttribution"}, + {"users", "switchUsers"}, + {"push", "switchPush"}, + {"starRating", "switchStarRating"}, + {"remoteConfig", "switchRemoteConfig"}, + {"location", "switchLocation"}, + {"feedback", "switchFeedback"}, + {"apm", "switchApm"}, + {"content", "switchContent"}, + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_example_consent); + + // Set initial switch states and listeners + for (String[] feature : FEATURES) { + String featureName = feature[0]; + int resId = getResources().getIdentifier(feature[1], "id", getPackageName()); + SwitchMaterial sw = findViewById(resId); + if (sw != null) { + sw.setChecked(Countly.sharedInstance().consent().getConsent(featureName)); + sw.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + Countly.sharedInstance().consent().giveConsent(new String[]{featureName}); + } else { + Countly.sharedInstance().consent().removeConsent(new String[]{featureName}); + } + }); + } + } + + findViewById(R.id.btnGiveAllConsent).setOnClickListener(v -> { + Countly.sharedInstance().consent().giveConsentAll(); + refreshSwitches(); + Toast.makeText(this, "All consent given", Toast.LENGTH_SHORT).show(); + }); + + findViewById(R.id.btnRemoveAllConsent).setOnClickListener(v -> { + Countly.sharedInstance().consent().removeConsentAll(); + refreshSwitches(); + Toast.makeText(this, "All consent removed", Toast.LENGTH_SHORT).show(); + }); + } + + private void refreshSwitches() { + for (String[] feature : FEATURES) { + int resId = getResources().getIdentifier(feature[1], "id", getPackageName()); + SwitchMaterial sw = findViewById(resId); + if (sw != null) { + sw.setChecked(Countly.sharedInstance().consent().getConsent(feature[0])); + } + } + } +} diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleContentZone.java b/app/src/main/java/ly/count/android/demo/ActivityExampleContentZone.java index 7d728f672..dc0b14478 100644 --- a/app/src/main/java/ly/count/android/demo/ActivityExampleContentZone.java +++ b/app/src/main/java/ly/count/android/demo/ActivityExampleContentZone.java @@ -38,5 +38,6 @@ public void onClickChangeDeviceIdContentZone(View v) { String newDeviceId = deviceId.isEmpty() ? UUID.randomUUID().toString() : deviceId; Countly.sharedInstance().deviceId().setID(newDeviceId); + Countly.sharedInstance().consent().giveConsentAll(); } } diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleCustomEvents.java b/app/src/main/java/ly/count/android/demo/ActivityExampleCustomEvents.java index cfbfbe7a6..fd2ead970 100644 --- a/app/src/main/java/ly/count/android/demo/ActivityExampleCustomEvents.java +++ b/app/src/main/java/ly/count/android/demo/ActivityExampleCustomEvents.java @@ -1,91 +1,165 @@ package ly.count.android.demo; import android.os.Bundle; +import android.view.LayoutInflater; import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Toast; + import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.textfield.TextInputEditText; + +import java.util.HashMap; import java.util.Map; -import java.util.Random; -import java.util.concurrent.ConcurrentHashMap; + import ly.count.android.sdk.Countly; -@SuppressWarnings("UnusedParameters") public class ActivityExampleCustomEvents extends AppCompatActivity { + private TextInputEditText inputEventName, inputEventCount, inputEventSum, inputEventDuration; + private TextInputEditText inputTimedEventName; + private LinearLayout segmentationContainer; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_example_custom_events); - } - public void onClickRecordEvent01(View v) { - Countly.sharedInstance().events().recordEvent("Custom event 1"); + inputEventName = findViewById(R.id.inputEventName); + inputEventCount = findViewById(R.id.inputEventCount); + inputEventSum = findViewById(R.id.inputEventSum); + inputEventDuration = findViewById(R.id.inputEventDuration); + inputTimedEventName = findViewById(R.id.inputTimedEventName); + segmentationContainer = findViewById(R.id.segmentationContainer); + + findViewById(R.id.btnAddSegment).setOnClickListener(v -> addSegmentRow()); + findViewById(R.id.btnRecordEvent).setOnClickListener(v -> recordCustomEvent()); + findViewById(R.id.btnStartTimedEvent).setOnClickListener(v -> startTimedEvent()); + findViewById(R.id.btnEndTimedEvent).setOnClickListener(v -> endTimedEvent()); + findViewById(R.id.btnCancelTimedEvent).setOnClickListener(v -> cancelTimedEvent()); + findViewById(R.id.btnPreset1).setOnClickListener(v -> recordPresetWithSegmentation()); + findViewById(R.id.btnPreset2).setOnClickListener(v -> recordPresetWithCountSum()); + findViewById(R.id.btnTriggerSend).setOnClickListener(v -> { + Countly.sharedInstance().requestQueue().attemptToSendStoredRequests(); + Toast.makeText(this, "Sending stored requests", Toast.LENGTH_SHORT).show(); + }); + + // Add one default segment row + addSegmentRow(); } - public void onClickRecordEvent02(View v) { - Countly.sharedInstance().events().recordEvent("Custom event 2", 3); + private void addSegmentRow() { + View row = LayoutInflater.from(this).inflate(R.layout.row_segmentation, segmentationContainer, false); + row.findViewById(R.id.btnRemoveSegment).setOnClickListener(v -> segmentationContainer.removeView(row)); + segmentationContainer.addView(row); } - public void onClickRecordEvent03(View v) { - Countly.sharedInstance().events().recordEvent("Custom event 3", 1, 134); + private Map collectSegmentation() { + Map segmentation = new HashMap<>(); + for (int i = 0; i < segmentationContainer.getChildCount(); i++) { + View row = segmentationContainer.getChildAt(i); + EditText keyField = row.findViewById(R.id.inputSegKey); + EditText valueField = row.findViewById(R.id.inputSegValue); + String key = keyField.getText().toString().trim(); + String value = valueField.getText().toString().trim(); + if (!key.isEmpty() && !value.isEmpty()) { + // Try to parse as number + try { + if (value.contains(".")) { + segmentation.put(key, Double.parseDouble(value)); + } else { + segmentation.put(key, Integer.parseInt(value)); + } + } catch (NumberFormatException e) { + segmentation.put(key, value); + } + } + } + return segmentation; } - public void onClickRecordEvent04(View v) { - Countly.sharedInstance().events().recordEvent("Custom event 4", null, 1, 0, 55); + private void recordCustomEvent() { + String eventName = inputEventName.getText() != null ? inputEventName.getText().toString().trim() : ""; + if (eventName.isEmpty()) { + inputEventName.setError("Event name is required"); + return; + } + + int count = 1; + double sum = 0; + double duration = 0; + + try { + String countStr = inputEventCount.getText() != null ? inputEventCount.getText().toString().trim() : ""; + if (!countStr.isEmpty()) count = Integer.parseInt(countStr); + } catch (NumberFormatException ignored) {} + + try { + String sumStr = inputEventSum.getText() != null ? inputEventSum.getText().toString().trim() : ""; + if (!sumStr.isEmpty()) sum = Double.parseDouble(sumStr); + } catch (NumberFormatException ignored) {} + + try { + String durStr = inputEventDuration.getText() != null ? inputEventDuration.getText().toString().trim() : ""; + if (!durStr.isEmpty()) duration = Double.parseDouble(durStr); + } catch (NumberFormatException ignored) {} + + Map segmentation = collectSegmentation(); + + if (segmentation.isEmpty()) { + Countly.sharedInstance().events().recordEvent(eventName, count, sum); + } else { + Countly.sharedInstance().events().recordEvent(eventName, segmentation, count, sum, duration); + } + + Toast.makeText(this, "Event '" + eventName + "' recorded", Toast.LENGTH_SHORT).show(); } - public void onClickRecordEvent05(View v) { - Map segmentation = new ConcurrentHashMap<>(); - segmentation.put("wall", "green"); - Countly.sharedInstance().events().recordEvent("Custom event 5", segmentation, 1, 0, 0); + private void startTimedEvent() { + String name = inputTimedEventName.getText() != null ? inputTimedEventName.getText().toString().trim() : ""; + if (name.isEmpty()) { + inputTimedEventName.setError("Event name is required"); + return; + } + boolean started = Countly.sharedInstance().events().startEvent(name); + Toast.makeText(this, started ? "Timed event '" + name + "' started" : "Could not start (already running?)", Toast.LENGTH_SHORT).show(); } - public void onClickRecordEvent06(View v) { - Map segmentation = new ConcurrentHashMap<>(); - segmentation.put("wall", "red"); - segmentation.put("flowers", 3); - segmentation.put("area", 1.23); - segmentation.put("volume", 7.88); - Countly.sharedInstance().events().recordEvent("Custom event 6", segmentation, 15, 0, 0); + private void endTimedEvent() { + String name = inputTimedEventName.getText() != null ? inputTimedEventName.getText().toString().trim() : ""; + if (name.isEmpty()) { + inputTimedEventName.setError("Event name is required"); + return; + } + boolean ended = Countly.sharedInstance().events().endEvent(name); + Toast.makeText(this, ended ? "Timed event '" + name + "' ended" : "Could not end (not running?)", Toast.LENGTH_SHORT).show(); } - public void onClickRecordEvent07(View v) { - Map segmentation = new ConcurrentHashMap<>(); - segmentation.put("wall", "blue"); - segmentation.put("flowers", new Random().nextInt()); - segmentation.put("area", new Random().nextDouble()); - segmentation.put("volume", new Random().nextDouble()); - - Countly.sharedInstance().events().recordEvent("Custom event 7", segmentation, 25, 10, 0); + private void cancelTimedEvent() { + String name = inputTimedEventName.getText() != null ? inputTimedEventName.getText().toString().trim() : ""; + if (name.isEmpty()) { + inputTimedEventName.setError("Event name is required"); + return; + } + boolean cancelled = Countly.sharedInstance().events().cancelEvent(name); + Toast.makeText(this, cancelled ? "Timed event '" + name + "' cancelled" : "Could not cancel (not running?)", Toast.LENGTH_SHORT).show(); } - public void onClickRecordEvent08(View v) { - Map segmentation = new ConcurrentHashMap<>(); - segmentation.put("wall", "yellow"); - Countly.sharedInstance().events().recordEvent("Custom event 8", segmentation, 25, 10, 50); - } - - public void onClickRecordEvent09(View v) { - //start timed event - Countly.sharedInstance().events().recordEvent("Custom event 9"); - } - - public void onClickRecordEvent10(View v) { - //stop timed event - Countly.sharedInstance().events().recordEvent("Custom event 9"); - } - - public void onClickRecordEvent12(View v) { - //cancel timed event - Countly.sharedInstance().events().cancelEvent("Custom event 9"); - } - - public void onClickRecordEvent11(View v) { - Map segmentation = new ConcurrentHashMap<>(); - segmentation.put("wall", "orange"); - Countly.sharedInstance().events().recordEvent("Custom event 9", segmentation, 4, 34); + private void recordPresetWithSegmentation() { + Map segmentation = new HashMap<>(); + segmentation.put("wall", "green"); + segmentation.put("flowers", 3); + Countly.sharedInstance().events().recordEvent("Preset Segmentation Event", segmentation, 1, 0, 0); + Toast.makeText(this, "Preset segmentation event recorded", Toast.LENGTH_SHORT).show(); } - public void onClickTriggerSendingEvents(View v) { - Countly.sharedInstance().requestQueue().attemptToSendStoredRequests(); + private void recordPresetWithCountSum() { + Map segmentation = new HashMap<>(); + segmentation.put("wall", "blue"); + Countly.sharedInstance().events().recordEvent("Preset Count Sum Event", segmentation, 5, 24.5, 0); + Toast.makeText(this, "Preset count+sum event recorded", Toast.LENGTH_SHORT).show(); } } diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleFragments.java b/app/src/main/java/ly/count/android/demo/ActivityExampleFragments.java new file mode 100644 index 000000000..069f23c64 --- /dev/null +++ b/app/src/main/java/ly/count/android/demo/ActivityExampleFragments.java @@ -0,0 +1,42 @@ +package ly.count.android.demo; + +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; + +public class ActivityExampleFragments extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_example_fragments); + + // Show Fragment A by default + if (savedInstanceState == null) { + showFragment(new DemoFragmentA(), false); + } + + findViewById(R.id.btn_fragment_a).setOnClickListener(v -> showFragment(new DemoFragmentA(), true)); + findViewById(R.id.btn_fragment_b).setOnClickListener(v -> showFragment(new DemoFragmentB(), true)); + findViewById(R.id.btn_fragment_c).setOnClickListener(v -> showFragment(new DemoFragmentC(), true)); + } + + private void showFragment(Fragment fragment, boolean addToBackStack) { + FragmentTransaction tx = getSupportFragmentManager() + .beginTransaction() + .setCustomAnimations( + android.R.anim.fade_in, + android.R.anim.fade_out, + android.R.anim.fade_in, + android.R.anim.fade_out + ) + .replace(R.id.fragment_container, fragment); + + if (addToBackStack) { + tx.addToBackStack(null); + } + + tx.commit(); + } +} diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleLocation.java b/app/src/main/java/ly/count/android/demo/ActivityExampleLocation.java new file mode 100644 index 000000000..8af88e93e --- /dev/null +++ b/app/src/main/java/ly/count/android/demo/ActivityExampleLocation.java @@ -0,0 +1,59 @@ +package ly.count.android.demo; + +import android.os.Bundle; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.textfield.TextInputEditText; + +import ly.count.android.sdk.Countly; + +public class ActivityExampleLocation extends AppCompatActivity { + + private TextInputEditText inputCountryCode, inputCity, inputLatitude, inputLongitude, inputIpAddress; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_example_location); + + inputCountryCode = findViewById(R.id.inputCountryCode); + inputCity = findViewById(R.id.inputCity); + inputLatitude = findViewById(R.id.inputLatitude); + inputLongitude = findViewById(R.id.inputLongitude); + inputIpAddress = findViewById(R.id.inputIpAddress); + + findViewById(R.id.btnSetLocation).setOnClickListener(v -> setLocation()); + findViewById(R.id.btnDisableLocation).setOnClickListener(v -> { + Countly.sharedInstance().location().disableLocation(); + Toast.makeText(this, "Location disabled", Toast.LENGTH_SHORT).show(); + }); + } + + private String getText(TextInputEditText field) { + return field.getText() != null ? field.getText().toString().trim() : ""; + } + + private void setLocation() { + String countryCode = getText(inputCountryCode); + String city = getText(inputCity); + String lat = getText(inputLatitude); + String lng = getText(inputLongitude); + String ip = getText(inputIpAddress); + + String gpsCoords = null; + if (!lat.isEmpty() && !lng.isEmpty()) { + gpsCoords = lat + "," + lng; + } + + Countly.sharedInstance().location().setLocation( + countryCode.isEmpty() ? null : countryCode, + city.isEmpty() ? null : city, + gpsCoords, + ip.isEmpty() ? null : ip + ); + + Toast.makeText(this, "Location set", Toast.LENGTH_SHORT).show(); + } +} diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleOthers.java b/app/src/main/java/ly/count/android/demo/ActivityExampleOthers.java index 4cbccbe72..80b343064 100644 --- a/app/src/main/java/ly/count/android/demo/ActivityExampleOthers.java +++ b/app/src/main/java/ly/count/android/demo/ActivityExampleOthers.java @@ -4,6 +4,7 @@ import android.os.Bundle; import android.util.Log; import android.view.View; +import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -24,30 +25,14 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_example_others); } - public void onClickViewOther05(View v) { - //set user location - String countryCode = "us"; - String city = "Höuston"; - String latitude = "29.634933"; - String longitude = "-95.220255"; - String ipAddress = null; - - Countly.sharedInstance().location().setLocation(countryCode, city, latitude + "," + longitude, ipAddress); - } - - public void onClickViewOther06(View v) { - //disable location - Countly.sharedInstance().location().disableLocation(); - } - - public void onClickViewOther08(View v) { - //Clearing request queue + public void onClickClearRequestQueue(View v) { Countly.sharedInstance().requestQueue().flushQueues(); + Toast.makeText(this, "Request queue cleared", Toast.LENGTH_SHORT).show(); } - public void onClickViewOther10(View v) { - //Doing internally stored requests + public void onClickSendStoredRequests(View v) { Countly.sharedInstance().requestQueue().attemptToSendStoredRequests(); + Toast.makeText(this, "Sending stored requests", Toast.LENGTH_SHORT).show(); } public void onAddDirectRequestClick(View v) { @@ -94,47 +79,28 @@ public void onAddDirectRequestClick(View v) { Log.e("Countly", "Failed to create JSON object", e); } Countly.sharedInstance().requestQueue().addDirectRequest(requestMap); + Toast.makeText(this, "Direct request added", Toast.LENGTH_SHORT).show(); } - public void onClickTestcrashFilterSample(View v) { + public void onClickTestCrashFilterSample(View v) { Countly.sharedInstance().crashes().recordUnhandledException(new Throwable("A really secret exception")); - } - - public void onClickRemoveAllConsent(View v) { - Countly.sharedInstance().consent().removeConsentAll(); - } - - public void onClickGiveAllConsent(View v) { - Countly.sharedInstance().consent().giveConsentAll(); + Toast.makeText(this, "Secret crash recorded (should be filtered)", Toast.LENGTH_SHORT).show(); } public void onClickReportDirectAttribution(View v) { Countly.sharedInstance().attribution().recordDirectAttribution("countly", "{'cid':'campaign_id', 'cuid':'campaign_user_id'}"); - } - - String GetAdvertisingID() { - return "12345";//this is only a dummy value + Toast.makeText(this, "Direct attribution reported", Toast.LENGTH_SHORT).show(); } public void onClickReportIndirectAttribution(View v) { Map attributionValues = new ConcurrentHashMap<>(); - attributionValues.put(AttributionIndirectKey.AdvertisingID, GetAdvertisingID()); + attributionValues.put(AttributionIndirectKey.AdvertisingID, "12345"); Countly.sharedInstance().attribution().recordIndirectAttribution(attributionValues); + Toast.makeText(this, "Indirect attribution reported", Toast.LENGTH_SHORT).show(); } public void onClickAttributionTest(View v) { Countly.sharedInstance().attribution().recordDirectAttribution("_special_test", "{'test_object':'some value', 'other value':'123'}"); - } - - public void onClickBeginSession(View v) { - Countly.sharedInstance().sessions().beginSession(); - } - - public void onClickUpdateSession(View v) { - Countly.sharedInstance().sessions().updateSession(); - } - - public void onClickEndSession(View v) { - Countly.sharedInstance().sessions().endSession(); + Toast.makeText(this, "Attribution test reported", Toast.LENGTH_SHORT).show(); } } diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleSessions.java b/app/src/main/java/ly/count/android/demo/ActivityExampleSessions.java new file mode 100644 index 000000000..265a5961e --- /dev/null +++ b/app/src/main/java/ly/count/android/demo/ActivityExampleSessions.java @@ -0,0 +1,32 @@ +package ly.count.android.demo; + +import android.os.Bundle; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import ly.count.android.sdk.Countly; + +public class ActivityExampleSessions extends AppCompatActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_example_sessions); + + findViewById(R.id.btnBeginSession).setOnClickListener(v -> { + Countly.sharedInstance().sessions().beginSession(); + Toast.makeText(this, "Session begun", Toast.LENGTH_SHORT).show(); + }); + + findViewById(R.id.btnUpdateSession).setOnClickListener(v -> { + Countly.sharedInstance().sessions().updateSession(); + Toast.makeText(this, "Session updated", Toast.LENGTH_SHORT).show(); + }); + + findViewById(R.id.btnEndSession).setOnClickListener(v -> { + Countly.sharedInstance().sessions().endSession(); + Toast.makeText(this, "Session ended", Toast.LENGTH_SHORT).show(); + }); + } +} diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleUserDetails.java b/app/src/main/java/ly/count/android/demo/ActivityExampleUserDetails.java index c1dbcd68f..964839596 100644 --- a/app/src/main/java/ly/count/android/demo/ActivityExampleUserDetails.java +++ b/app/src/main/java/ly/count/android/demo/ActivityExampleUserDetails.java @@ -1,90 +1,207 @@ package ly.count.android.demo; import android.os.Bundle; +import android.view.LayoutInflater; import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.textfield.TextInputEditText; + import java.util.HashMap; +import java.util.Map; import ly.count.android.sdk.Countly; -@SuppressWarnings("UnusedParameters") public class ActivityExampleUserDetails extends AppCompatActivity { + private TextInputEditText inputName, inputUsername, inputEmail, inputOrganization; + private TextInputEditText inputPhone, inputGender, inputBirthYear, inputPictureUrl; + private TextInputEditText inputOpKey, inputOpValue; + private LinearLayout customPropsContainer; + private Spinner spinnerOperation; + + private static final String[] OPERATIONS = { + "increment", "incrementBy", "multiply", "saveMax", "saveMin", + "setOnce", "push", "pushUnique", "pull" + }; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_example_user_details); - } - - public void onClickUserData01(View v) { - setUserData(); - } - - public void onClickUserData02(View v) { - //providing any custom key values to store with user - HashMap custom = new HashMap<>(); - custom.put("favoriteAnimal", "dog"); - - //set multiple custom properties - Countly.sharedInstance().userProfile().setProperties(custom); - Countly.sharedInstance().userProfile().save(); - } - public void onClickUserData03(View v) { - //providing any custom key values to store with user - HashMap custom = new HashMap<>(); - custom.put("leastFavoritePet", "cat"); - - //set multiple custom properties - Countly.sharedInstance().userProfile().setProperties(custom); - Countly.sharedInstance().userProfile().save(); + inputName = findViewById(R.id.inputName); + inputUsername = findViewById(R.id.inputUsername); + inputEmail = findViewById(R.id.inputEmail); + inputOrganization = findViewById(R.id.inputOrganization); + inputPhone = findViewById(R.id.inputPhone); + inputGender = findViewById(R.id.inputGender); + inputBirthYear = findViewById(R.id.inputBirthYear); + inputPictureUrl = findViewById(R.id.inputPictureUrl); + inputOpKey = findViewById(R.id.inputOpKey); + inputOpValue = findViewById(R.id.inputOpValue); + customPropsContainer = findViewById(R.id.customPropsContainer); + spinnerOperation = findViewById(R.id.spinnerOperation); + + ArrayAdapter adapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_dropdown_item, OPERATIONS); + spinnerOperation.setAdapter(adapter); + + findViewById(R.id.btnAddCustomProp).setOnClickListener(v -> addCustomPropRow()); + findViewById(R.id.btnSetStandardProps).setOnClickListener(v -> setStandardProperties()); + findViewById(R.id.btnSetCustomProps).setOnClickListener(v -> setCustomProperties()); + findViewById(R.id.btnExecuteOp).setOnClickListener(v -> executeOperation()); + findViewById(R.id.btnSaveProfile).setOnClickListener(v -> { + Countly.sharedInstance().userProfile().save(); + Toast.makeText(this, "Profile saved to server", Toast.LENGTH_SHORT).show(); + }); + findViewById(R.id.btnClearProfile).setOnClickListener(v -> { + Countly.sharedInstance().userProfile().clear(); + Toast.makeText(this, "Profile modifications cleared", Toast.LENGTH_SHORT).show(); + }); + + // Add one default row + addCustomPropRow(); } - public void onClickUserData04(View v) { - + private void addCustomPropRow() { + // Reuse the same row_segmentation layout (key-value pair with remove button) + View row = LayoutInflater.from(this).inflate(R.layout.row_segmentation, customPropsContainer, false); + row.findViewById(R.id.btnRemoveSegment).setOnClickListener(v -> customPropsContainer.removeView(row)); + customPropsContainer.addView(row); } - public void onClickUserData05(View v) { - + private String getText(TextInputEditText field) { + return field.getText() != null ? field.getText().toString().trim() : ""; } - public void setUserData() { + private void setStandardProperties() { HashMap data = new HashMap<>(); - data.put("name", "First name Last name"); - data.put("username", "nickname"); - data.put("email", "test@test.com"); - data.put("organization", "Tester"); - data.put("phone", "+123456789"); - data.put("gender", "M"); - //provide url to picture - //data.put("picture", "http://example.com/pictures/profile_pic.png"); - //or locally from device - //data.put("picturePath", "/mnt/sdcard/portrait.jpg"); - data.put("byear", "1987"); - - //providing any custom key values to store with user - data.put("Top rated Country", "Turkey"); - data.put("Favourite city", "Istanbul"); - data.put("Favourite car", "VroomVroom"); - - //set multiple custom properties - Countly.sharedInstance().userProfile().setProperties(data); - //set custom properties by one - Countly.sharedInstance().userProfile().setProperty("test", "test"); + String name = getText(inputName); + String username = getText(inputUsername); + String email = getText(inputEmail); + String org = getText(inputOrganization); + String phone = getText(inputPhone); + String gender = getText(inputGender); + String byear = getText(inputBirthYear); + String picture = getText(inputPictureUrl); + + if (!name.isEmpty()) data.put("name", name); + if (!username.isEmpty()) data.put("username", username); + if (!email.isEmpty()) data.put("email", email); + if (!org.isEmpty()) data.put("organization", org); + if (!phone.isEmpty()) data.put("phone", phone); + if (!gender.isEmpty()) data.put("gender", gender); + if (!byear.isEmpty()) data.put("byear", byear); + if (!picture.isEmpty()) data.put("picture", picture); + + if (data.isEmpty()) { + Toast.makeText(this, "Please fill at least one field", Toast.LENGTH_SHORT).show(); + return; + } - //increment used value by 1 - Countly.sharedInstance().userProfile().incrementBy("used", 1); + Countly.sharedInstance().userProfile().setProperties(data); + Toast.makeText(this, "Standard properties set (" + data.size() + " fields)", Toast.LENGTH_SHORT).show(); + } - //insert value to array of unique values - Countly.sharedInstance().userProfile().pushUnique("type", "morning"); + private void setCustomProperties() { + HashMap custom = new HashMap<>(); + for (int i = 0; i < customPropsContainer.getChildCount(); i++) { + View row = customPropsContainer.getChildAt(i); + EditText keyField = row.findViewById(R.id.inputSegKey); + EditText valueField = row.findViewById(R.id.inputSegValue); + String key = keyField.getText().toString().trim(); + String value = valueField.getText().toString().trim(); + if (!key.isEmpty() && !value.isEmpty()) { + try { + if (value.contains(".")) { + custom.put(key, Double.parseDouble(value)); + } else { + custom.put(key, Integer.parseInt(value)); + } + } catch (NumberFormatException e) { + custom.put(key, value); + } + } + } + + if (custom.isEmpty()) { + Toast.makeText(this, "Please add at least one property", Toast.LENGTH_SHORT).show(); + return; + } - //insert multiple values to same property - Countly.sharedInstance().userProfile().pushUnique("skill", "fire"); - Countly.sharedInstance().userProfile().pushUnique("skill", "earth"); + Countly.sharedInstance().userProfile().setProperties(custom); + Toast.makeText(this, "Custom properties set (" + custom.size() + " fields)", Toast.LENGTH_SHORT).show(); + } - Countly.sharedInstance().userProfile().save(); + private void executeOperation() { + String key = getText(inputOpKey); + String value = getText(inputOpValue); + + if (key.isEmpty()) { + inputOpKey.setError("Key is required"); + return; + } + + String operation = OPERATIONS[spinnerOperation.getSelectedItemPosition()]; + + switch (operation) { + case "increment": + Countly.sharedInstance().userProfile().increment(key); + break; + case "incrementBy": + try { + Countly.sharedInstance().userProfile().incrementBy(key, Integer.parseInt(value)); + } catch (NumberFormatException e) { + Toast.makeText(this, "Value must be a number", Toast.LENGTH_SHORT).show(); + return; + } + break; + case "multiply": + try { + Countly.sharedInstance().userProfile().multiply(key, Integer.parseInt(value)); + } catch (NumberFormatException e) { + Toast.makeText(this, "Value must be a number", Toast.LENGTH_SHORT).show(); + return; + } + break; + case "saveMax": + try { + Countly.sharedInstance().userProfile().saveMax(key, Integer.parseInt(value)); + } catch (NumberFormatException e) { + Toast.makeText(this, "Value must be a number", Toast.LENGTH_SHORT).show(); + return; + } + break; + case "saveMin": + try { + Countly.sharedInstance().userProfile().saveMin(key, Integer.parseInt(value)); + } catch (NumberFormatException e) { + Toast.makeText(this, "Value must be a number", Toast.LENGTH_SHORT).show(); + return; + } + break; + case "setOnce": + Countly.sharedInstance().userProfile().setOnce(key, value); + break; + case "push": + Countly.sharedInstance().userProfile().push(key, value); + break; + case "pushUnique": + Countly.sharedInstance().userProfile().pushUnique(key, value); + break; + case "pull": + Countly.sharedInstance().userProfile().pull(key, value); + break; + } + + Toast.makeText(this, operation + " applied to '" + key + "'", Toast.LENGTH_SHORT).show(); } } - diff --git a/app/src/main/java/ly/count/android/demo/DemoFragmentA.java b/app/src/main/java/ly/count/android/demo/DemoFragmentA.java new file mode 100644 index 000000000..a79a99188 --- /dev/null +++ b/app/src/main/java/ly/count/android/demo/DemoFragmentA.java @@ -0,0 +1,42 @@ +package ly.count.android.demo; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +public class DemoFragmentA extends Fragment { + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_demo_a, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + view.findViewById(R.id.btn_action_a1).setOnClickListener(v -> + Toast.makeText(requireContext(), "Button 1 tapped - touch works!", Toast.LENGTH_SHORT).show() + ); + + view.findViewById(R.id.btn_action_a2).setOnClickListener(v -> + Toast.makeText(requireContext(), "Button 2 tapped - touch works!", Toast.LENGTH_SHORT).show() + ); + + // Populate list to test scrolling with overlay + String[] items = new String[30]; + for (int i = 0; i < 30; i++) { + items[i] = "List item " + (i + 1) + " - test scrolling with overlay"; + } + ListView listView = view.findViewById(R.id.list_a); + listView.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, items)); + } +} diff --git a/app/src/main/java/ly/count/android/demo/DemoFragmentB.java b/app/src/main/java/ly/count/android/demo/DemoFragmentB.java new file mode 100644 index 000000000..234a1eed6 --- /dev/null +++ b/app/src/main/java/ly/count/android/demo/DemoFragmentB.java @@ -0,0 +1,28 @@ +package ly.count.android.demo; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +public class DemoFragmentB extends Fragment { + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_demo_b, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + view.findViewById(R.id.btn_action_b1).setOnClickListener(v -> + Toast.makeText(requireContext(), "Touch event works in Fragment B!", Toast.LENGTH_SHORT).show() + ); + } +} diff --git a/app/src/main/java/ly/count/android/demo/DemoFragmentC.java b/app/src/main/java/ly/count/android/demo/DemoFragmentC.java new file mode 100644 index 000000000..c19b35210 --- /dev/null +++ b/app/src/main/java/ly/count/android/demo/DemoFragmentC.java @@ -0,0 +1,43 @@ +package ly.count.android.demo; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +public class DemoFragmentC extends Fragment { + + private int navCount = 0; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_demo_c, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + TextView logText = view.findViewById(R.id.text_log); + + // Navigate to a different activity (tests overlay migration activity + fragment combo) + view.findViewById(R.id.btn_go_to_activity).setOnClickListener(v -> { + navCount++; + logText.append("\n[" + navCount + "] Navigating to Custom Events Activity..."); + startActivity(new Intent(requireContext(), ActivityExampleCustomEvents.class)); + }); + + // Pop back stack (tests overlay during fragment back navigation) + view.findViewById(R.id.btn_go_back).setOnClickListener(v -> { + navCount++; + logText.append("\n[" + navCount + "] Popping back stack..."); + requireActivity().getSupportFragmentManager().popBackStack(); + }); + } +} diff --git a/app/src/main/java/ly/count/android/demo/MainActivity.java b/app/src/main/java/ly/count/android/demo/MainActivity.java index 4b3cd2e78..d0d30cf80 100644 --- a/app/src/main/java/ly/count/android/demo/MainActivity.java +++ b/app/src/main/java/ly/count/android/demo/MainActivity.java @@ -4,7 +4,10 @@ import android.os.Bundle; import android.util.Log; import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; import com.android.installreferrer.api.InstallReferrerClient; import com.android.installreferrer.api.InstallReferrerStateListener; import com.android.installreferrer.api.ReferrerDetails; @@ -21,18 +24,19 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - /* - To send Referrer follow these steps. - Steps: - 1. Start Google Play on the device using campaign link, - for example, https://play.google.com/store/apps/details?id=ly.count.android.demo&referrer=utm_source%3Dtest_source%26utm_medium%3Dtest_medium%26 - utm_term%3Dtest-term%26utm_content%3Dtest_content%26utm_campaign%3Dtest_name - (You can use google play generator: https://developers.google.com/analytics/devguides/collection/android/v3/campaigns#google-play-url-builder) - 2. DON'T TAP ON INSTALL BUTTON - 3. Install your test build using adb. - - Google Play will be returning your test campaign now. - */ + TextView txtVersion = findViewById(R.id.txtSdkVersion); + txtVersion.setText("SDK v" + Countly.sharedInstance().COUNTLY_SDK_VERSION_STRING); + + ImageView btnNightMode = findViewById(R.id.btnToggleNightMode); + btnNightMode.setOnClickListener(v -> { + int currentMode = AppCompatDelegate.getDefaultNightMode(); + if (currentMode == AppCompatDelegate.MODE_NIGHT_YES) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } + }); + referrerClient = InstallReferrerClient.newBuilder(this).build(); referrerClient.startConnection(new InstallReferrerStateListener() { @Override @@ -41,21 +45,11 @@ public void onInstallReferrerSetupFinished(int responseCode) { case InstallReferrerClient.InstallReferrerResponse.OK: try { ReferrerDetails response = referrerClient.getInstallReferrer(); - - //you would retrieve the referrer url String referrerUrl = response.getInstallReferrer(); - - //and then you would parse it and retrieve the required field to identify the campaign id and user id String campaignId = "someCampaignId"; String userId = "someUserId"; - - // The string is usually URL Encoded, so we need to decode it. String referrer = URLDecoder.decode(referrerUrl, "UTF-8"); - - // Log the referrer string. Log.d(Countly.TAG, "Received Referrer url: " + referrer); - - //retrieve specific parts from the referrer url String[] parts = referrer.split("&"); for (String part : parts) { if (part.startsWith("countly_cid")) { @@ -65,21 +59,15 @@ public void onInstallReferrerSetupFinished(int responseCode) { userId = part.replace("countly_cuid=", "").trim(); } } - - //you would then pass those retrieved values as manual attribution: Countly.sharedInstance().attribution().recordDirectAttribution("countly", "{\"cid\":" + campaignId + ",\"cuid\":" + userId + "}"); - //Countly.sharedInstance().attribution().recordDirectAttribution(campaignId, userId); - referrerClient.endConnection(); } catch (Exception e) { Log.e(demoTag, "Error while parsing referrer", e); } break; case InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED: - // API not available on the current Play Store app. break; case InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE: - // Connection couldn't be established. break; default: break; @@ -90,8 +78,6 @@ public void onInstallReferrerSetupFinished(int responseCode) { public void onInstallReferrerServiceDisconnected() { } }); - - //ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS }, 123); } public void onClickButtonCustomEvents(View v) { @@ -118,10 +104,6 @@ public void onClickButtonAutoViewTracking(View v) { startActivity(new Intent(this, ActivityExampleAutoViewTracking.class)); } - public void onClickButtonMultiThreading(View v) { - // - } - public void onClickButtonOthers(View v) { startActivity(new Intent(this, ActivityExampleOthers.class)); } @@ -145,4 +127,20 @@ public void onClickButtonRatings(View v) { public void onClickContentZone(View v) { startActivity(new Intent(this, ActivityExampleContentZone.class)); } + + public void onClickButtonFragments(View v) { + startActivity(new Intent(this, ActivityExampleFragments.class)); + } + + public void onClickButtonConsent(View v) { + startActivity(new Intent(this, ActivityExampleConsent.class)); + } + + public void onClickButtonLocation(View v) { + startActivity(new Intent(this, ActivityExampleLocation.class)); + } + + public void onClickButtonSessions(View v) { + startActivity(new Intent(this, ActivityExampleSessions.class)); + } } diff --git a/app/src/main/res/drawable/bg_card_strip_rounded.xml b/app/src/main/res/drawable/bg_card_strip_rounded.xml new file mode 100644 index 000000000..ff2bcce93 --- /dev/null +++ b/app/src/main/res/drawable/bg_card_strip_rounded.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/bg_header_gradient.xml b/app/src/main/res/drawable/bg_header_gradient.xml new file mode 100644 index 000000000..0e506b491 --- /dev/null +++ b/app/src/main/res/drawable/bg_header_gradient.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_night_mode.xml b/app/src/main/res/drawable/ic_night_mode.xml new file mode 100644 index 000000000..f6dbb4c75 --- /dev/null +++ b/app/src/main/res/drawable/ic_night_mode.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_example_consent.xml b/app/src/main/res/layout/activity_example_consent.xml new file mode 100644 index 000000000..6bb3ef7c1 --- /dev/null +++ b/app/src/main/res/layout/activity_example_consent.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_example_custom_events.xml b/app/src/main/res/layout/activity_example_custom_events.xml index dbab62414..77f908b37 100644 --- a/app/src/main/res/layout/activity_example_custom_events.xml +++ b/app/src/main/res/layout/activity_example_custom_events.xml @@ -1,135 +1,248 @@ - - - + android:fillViewport="true"> - -