diff --git a/.github/scripts/public-api.init.gradle b/.github/scripts/public-api.init.gradle new file mode 100644 index 00000000..203377c4 --- /dev/null +++ b/.github/scripts/public-api.init.gradle @@ -0,0 +1,42 @@ +// Applies metalava to the modules listed in -DapiModules (for example :sdk,:sdk-native) and points each module's +// signature file at <-DapiDir>/.txt: `metalavaGenerateSignature` writes the module's public API +// there, and `metalavaCheckCompatibility` checks the module's code against the signature found there. +// Only .github/workflows/public_api.yml applies it, through --init-script, so no build file changes. +def apiModules = (System.getProperty('apiModules') ?: '').tokenize(',') as Set +def apiDir = System.getProperty('apiDir') + +rootProject { + // On the root build script classpath the plugin shares a class loader with the Android Gradle Plugin it calls. + buildscript { + repositories { + gradlePluginPortal { + content { + includeGroup 'me.tylerbwong.gradle.metalava' + } + } + } + dependencies { + classpath 'me.tylerbwong.gradle.metalava:plugin:0.5.1' + } + } +} + +allprojects { project -> + if (!apiModules.contains(project.path)) { + return + } + if (!apiDir) { + throw new org.gradle.api.GradleException('Pass -DapiDir= to choose where the API signatures live.') + } + ['java-library', 'com.android.library'].each { pluginId -> + project.pluginManager.withPlugin(pluginId) { + project.pluginManager.apply('me.tylerbwong.gradle.metalava') + project.metalava { + filename.set(new File(apiDir, "${project.name}.txt").absolutePath) + enforceCheck.set(false) + // Public methods that return package-private types are lint findings, not compatibility breaks. + arguments.addAll('--hide', 'ReferencesHidden,HiddenTypeParameter') + } + } + } +} diff --git a/.github/scripts/public_api.py b/.github/scripts/public_api.py new file mode 100644 index 00000000..3019d5d7 --- /dev/null +++ b/.github/scripts/public_api.py @@ -0,0 +1,142 @@ +"""Keeps the metalava compatibility errors that touch the public API integrators can reach.""" +import argparse +import glob +import os +import re +import sys + +ERROR = re.compile(r"^(?P\S+?\.txt):(?P\d+): error: (?P.*)$") +PACKAGE = re.compile(r"^package (?P[\w.]+) \{$") +CLASS = re.compile(r"^ (?:@\S+\s+)*(?:(?:public|protected|static|final|abstract|sealed)\s+)*" + r"(?:class|interface|enum|@interface)\s+(?P[\w.$]+)") +REFERENCE = re.compile(r"\bly\.count(?:\.[A-Za-z_$][\w$]*)+") +ANSI = re.compile(r"\x1b\[[0-9;]*m") + + +def _in_package(name, package): + """Returns whether the class name, nested or not, is declared directly in package and not in a subpackage.""" + return name.startswith(f"{package}.") and name[len(package) + 1:][:1].isupper() + + +class Signature: + """One metalava signature file: its class blocks and the classes each class's signature names.""" + + def __init__(self, path): + """Parses the signature file at path.""" + self.blocks = [] + self.references = {} + self.headers = {} + self.members = {} + package, current, start = None, None, 0 + with open(path, encoding="utf-8") as handle: + lines = handle.read().splitlines() + for number, text in enumerate(lines, start=1): + package_match, class_match = PACKAGE.match(text), CLASS.match(text) + if package_match: + package = package_match.group("name") + elif class_match and package: + current, start = f"{package}.{class_match.group('name')}", number + self.references[current] = set(REFERENCE.findall(text)) + self.headers[current] = text.strip() + self.members[current] = [] + elif current and text == " }": + self.blocks.append((start, number, current)) + current = None + elif current: + self.references[current].update(REFERENCE.findall(text)) + if text.strip(): + self.members[current].append(text.strip()) + + def class_at(self, line): + """Returns the class whose block contains line, or None for a line outside every class.""" + for start, end, name in self.blocks: + if start <= line <= end: + return name + return None + + +def reachable(signatures, root_packages): + """Returns the root package classes, the classes their signatures name, transitively, and nested classes.""" + references = {} + for signature in signatures: + for name, named in signature.references.items(): + references.setdefault(name, set()).update(named) + classes = set(references) + pending = [name for name in classes if any(_in_package(name, root) for root in root_packages)] + found = set() + while pending: + name = pending.pop() + if name in found: + continue + found.add(name) + pending.extend(other for other in classes if other.startswith(f"{name}.")) + pending.extend(reference for reference in references.get(name, ()) if reference in classes) + return found + + +def added(base_dir, head_dir, root_packages): + """Returns "class: declaration" lines for the public API the head signatures add to the base ones.""" + heads = {os.path.basename(path): Signature(path) for path in sorted(glob.glob(os.path.join(head_dir, "*.txt")))} + public = reachable(heads.values(), root_packages) + lines = [] + for name, head in heads.items(): + base_path = os.path.join(base_dir, name) + if not os.path.exists(base_path): + lines.append(f"{name[:-len('.txt')]}: a new module, all of its public API is new") + continue + base = Signature(base_path) + for owner in sorted(cls for cls in head.members if cls in public): + known = set(base.members.get(owner, ())) + if owner not in base.members: + lines.append(f"{owner}: {head.headers[owner]}") + lines.extend(f"{owner}: {member}" for member in head.members[owner] if member not in known) + return lines + + +def main(): + """Reads a metalava check log and prints the compatibility errors that touch reachable public API.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root-package", action="append", required=True, + help="a package whose classes are public API; repeat it for each root package") + parser.add_argument("--signatures", required=True, + help="every module's base signatures: one module's public API can name another's classes") + parser.add_argument("--module-dir", + help="the module's directory: metalava names this pull request's signature relative to it") + parser.add_argument("--added", metavar="HEAD_DIR", + help="print what the head signatures in this directory add to --signatures") + parser.add_argument("log", nargs="?", help="the metalava compatibility check log") + arguments = parser.parse_args() + if arguments.added: + for line in added(arguments.signatures, arguments.added, set(arguments.root_package)): + print(line) + return + if not arguments.module_dir or not arguments.log: + parser.error("--module-dir and the log are needed unless --added is given") + base = [Signature(path) for path in sorted(glob.glob(os.path.join(arguments.signatures, "*.txt")))] + if not base: + sys.exit(f"No base signatures in {arguments.signatures}.") + public = reachable(base, set(arguments.root_package)) + signatures = {} + found = 0 + with open(arguments.log, encoding="utf-8", errors="replace") as handle: + for raw in handle: + match = ERROR.match(ANSI.sub("", raw.strip())) + if not match: + continue + found += 1 + path = os.path.join(arguments.module_dir, match.group("path")) + message = match.group("message") + if path not in signatures: + signatures[path] = Signature(path) + owner = signatures[path].class_at(int(match.group("line"))) + if owner is None or owner in public: + print(message) + else: + # A class that is public only so other SDK packages can call it is not integrator API. + print(f"Ignored, {owner} is not reachable from the public API: {message}", file=sys.stderr) + if found == 0: + sys.exit("No metalava error lines found in the log.") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/public_api_record.sh b/.github/scripts/public_api_record.sh new file mode 100644 index 00000000..88f5c950 --- /dev/null +++ b/.github/scripts/public_api_record.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Compares the public API this run extracted with the record committed in the repository. On a difference it lists the +# changed lines in the job summary, copies the extracted API to $RUNNER_TEMP/public-api-record/ under the record's path, +# for the workflow to upload, and fails: a deliberate change is committed together with a changelog line. +# $@ pairs "=" +set -euo pipefail +out="$RUNNER_TEMP/public-api-record" +status=0 +current="$RUNNER_TEMP/public-api-current.txt" +recorded="$RUNNER_TEMP/public-api-recorded.txt" +for pair in "$@"; do + extracted=${pair%%=*} + record=${pair#*=} + # Line endings do not count: .NET writes CRLF on Windows, and a Windows checkout can give the record CRLF too. + tr -d '\r' < "$extracted" > "$current" + if [ -f "$record" ] && tr -d '\r' < "$record" | cmp -s - "$current"; then + echo "The public API is the one $record records." + continue + fi + status=1 + mkdir -p "$out/$(dirname "$record")" + cp "$current" "$out/$record" + { + printf '### The public API differs from %s\n\n' "$record" + printf "Commit \`%s\` from the \`public-api-record\` artifact of this run, with a changelog line for the change.\n\n" "$record" + if [ -f "$record" ]; then + printf '```diff\n' + tr -d '\r' < "$record" > "$recorded" + diff -u "$recorded" "$current" | tail -n +3 | head -n 300 || true + printf '```\n\n' + else + printf 'There is no record yet.\n\n' + fi + } >> "$GITHUB_STEP_SUMMARY" + echo "::error title=Public API record::The public API differs from $record. Commit the file from this run's public-api-record artifact and add a changelog line." +done +exit "$status" diff --git a/.github/scripts/public_api_report.sh b/.github/scripts/public_api_report.sh new file mode 100644 index 00000000..e9b930c3 --- /dev/null +++ b/.github/scripts/public_api_report.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Writes the report of a pull request's public API changes to the job summary, and to the step output "report", which +# the workflow's comment job posts on the pull request. The comment lists at most 200 lines per section. +# $1 a file with one removed or changed declaration per line +# $2 a file with one declaration per line that the pull request adds to the public API +set -euo pipefail +breaking=$1 +added=$2 +marker='' + +# Prints a section titled $1 and explained by $2 with the lines of file $3, at most $4 of them when $4 is not 0. +section() { + local count + count=$(grep -c . "$3" || true) + [ "$count" -gt 0 ] || return 0 + printf '**%s: %s.** %s\n\n```\n' "$1" "$count" "$2" + if [ "$4" -gt 0 ]; then + head -n "$4" "$3" + else + cat "$3" + fi + printf '```\n' + if [ "$4" -gt 0 ] && [ "$count" -gt "$4" ]; then + printf '\n%s more in the job summary.\n' "$((count - $4))" + fi + printf '\n' +} + +# Prints the whole report, with at most $1 lines per section when $1 is not 0. +report() { + printf '%s\n### Public API changes\n\n' "$marker" + if ! grep -q . "$breaking" && ! grep -q . "$added"; then + printf 'No public API change.\n' + return + fi + section 'Removed or changed' 'Each needs a CHANGELOG entry that marks it as a breaking change.' "$breaking" "$1" + section 'Added' 'Check that each one is meant to be public.' "$added" "$1" +} + +report 0 >> "$GITHUB_STEP_SUMMARY" +delimiter="EOF_$(od -An -N8 -tx1 /dev/urandom | tr -d ' \n')" +{ + echo "report<<$delimiter" + report 200 + echo "$delimiter" +} >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/api-changelog.yml b/.github/workflows/api-changelog.yml new file mode 100644 index 00000000..c827151a --- /dev/null +++ b/.github/workflows/api-changelog.yml @@ -0,0 +1,53 @@ +name: Public API changelog + +# A pull request that changes or removes a record of the public API, or of the requests the SDK sends, must also change +# the CHANGELOG.md that API_FILES pairs it with, so that the release notes tell integrators about every such change. A +# record the pull request adds needs no line. A path ending in "/" stands for every file under it. The API and request +# checks keep each record in step with the SDK. +on: + pull_request: + branches: [master, staging] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + pull-requests: read # to list the files the pull request changes + +jobs: + changelog: + name: A public API or request change comes with a CHANGELOG line + runs-on: ubuntu-24.04 + steps: + - name: Check the files the pull request changes + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + API_FILES: sdk-java/api/sdk-java.txt=CHANGELOG.md sdk-java-ui/api/sdk-java-ui.txt=sdk-java-ui/CHANGELOG.md sdk-java/api/requests/=CHANGELOG.md + run: | + changed=$(gh api "repos/$REPO/pulls/$PR/files" --paginate --jq '.[] | select(.status != "added") | .filename') + changes() { + case "$1" in + */) printf '%s\n' "$changed" | awk -v prefix="$1" 'index($0, prefix) == 1 { found = 1 } END { exit !found }' ;; + *) printf '%s\n' "$changed" | grep -qxF "$1" ;; + esac + } + recorded_changed=no + for pair in $API_FILES; do + record=${pair%%=*} + changelog=${pair#*=} + if changes "$record"; then + recorded_changed=yes + if ! changes "$changelog"; then + echo "::error::This pull request changes $record, which records the public API or the requests the SDK sends, but not $changelog. Add a line that tells integrators what changed." + exit 1 + fi + fi + done + if [ "$recorded_changed" = yes ]; then + echo "The recorded public API or requests change, and the CHANGELOG changes with them." + else + echo "This pull request changes neither the recorded public API nor the recorded requests." + fi diff --git a/.github/workflows/public_api.yml b/.github/workflows/public_api.yml new file mode 100644 index 00000000..dfc78999 --- /dev/null +++ b/.github/workflows/public_api.yml @@ -0,0 +1,152 @@ +name: Public API changes + +on: + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + api: + name: Breaking changes + outputs: + report: ${{ steps.report.outputs.report }} + runs-on: ubuntu-24.04 + env: + # Each module is paired with the changelog that records its breaking changes. + API_MODULES: ":sdk-java=CHANGELOG.md,:sdk-java-ui=sdk-java-ui/CHANGELOG.md" + steps: + - name: Checkout SDK code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 2 + persist-credentials: false + + - name: Set up Java + uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 + with: + distribution: corretto + java-version: 17 + + # The first parent of the pull request's merge commit is the base branch it merges into. + # A module the base branch does not have yet has no API to break, so it is left out. + - name: Write the public API of the base branch + run: | + git worktree add "$RUNNER_TEMP/base" HEAD^1 + cd "$RUNNER_TEMP/base" + IFS=, read -ra entries <<< "$API_MODULES" + modules=() + for entry in "${entries[@]}"; do + module="${entry%%=*}" + if [ -d "${module#:}" ]; then + modules+=("$module") + fi + done + ./gradlew --init-script "$GITHUB_WORKSPACE/.github/scripts/public-api.init.gradle" \ + -DapiModules="$(IFS=,; echo "${modules[*]}")" -DapiDir="$RUNNER_TEMP/api" \ + "${modules[@]/%/:metalavaGenerateSignature}" + + - name: Compare the public API with the base branch + id: compare + run: | + : > "$RUNNER_TEMP/breaking.txt" + : > "$RUNNER_TEMP/changelogs.txt" + IFS=, read -ra entries <<< "$API_MODULES" + for entry in "${entries[@]}"; do + module="${entry%%=*}" + if [ ! -f "$RUNNER_TEMP/api/${module#:}.txt" ]; then + continue + fi + if ./gradlew --init-script .github/scripts/public-api.init.gradle \ + -DapiModules="$module" -DapiDir="$RUNNER_TEMP/api" \ + "$module:metalavaCheckCompatibility" > "$RUNNER_TEMP/check.log" 2>&1; then + continue + fi + if ! grep -q 'Found compatibility problems' "$RUNNER_TEMP/check.log"; then + cat "$RUNNER_TEMP/check.log" + echo "::error::Could not compare the public API of $module with the base branch." + exit 1 + fi + python3 .github/scripts/public_api.py --root-package ly.count.sdk.java --root-package ly.count.sdk.java.ui \ + --signatures "$RUNNER_TEMP/api" --module-dir "${module#:}" "$RUNNER_TEMP/check.log" > "$RUNNER_TEMP/module.txt" + if [ -s "$RUNNER_TEMP/module.txt" ]; then + cat "$RUNNER_TEMP/module.txt" >> "$RUNNER_TEMP/breaking.txt" + echo "${entry#*=}" >> "$RUNNER_TEMP/changelogs.txt" + fi + done + # The head's own signatures, beside the base's, give what this pull request adds to the public API. + modules=() + for entry in "${entries[@]}"; do + modules+=("${entry%%=*}") + done + ./gradlew --init-script .github/scripts/public-api.init.gradle -DapiModules="$(IFS=,; echo "${modules[*]}")" \ + -DapiDir="$RUNNER_TEMP/api-head" "${modules[@]/%/:metalavaGenerateSignature}" + python3 .github/scripts/public_api.py --root-package ly.count.sdk.java --root-package ly.count.sdk.java.ui \ + --signatures "$RUNNER_TEMP/api" --added "$RUNNER_TEMP/api-head" > "$RUNNER_TEMP/added.txt" + + - name: Report the public API changes + id: report + run: bash .github/scripts/public_api_report.sh "$RUNNER_TEMP/breaking.txt" "$RUNNER_TEMP/added.txt" + + - name: Require a changelog entry for breaking changes + run: | + if [ ! -s "$RUNNER_TEMP/breaking.txt" ]; then + exit 0 + fi + missing="" + while read -r changelog; do + if git diff --quiet HEAD^1 HEAD -- "$changelog"; then + missing="$missing $changelog" + fi + done < <(sort -u "$RUNNER_TEMP/changelogs.txt") + if [ -n "$missing" ]; then + echo "::error title=Breaking API change::This pull request breaks the public API without an entry in:$missing. The job summary lists each break." + exit 1 + fi + echo "::warning title=Breaking API change::This pull request breaks the public API. Check that the changelog marks it as a breaking change." + + # The committed record keeps every change to the public API visible in the pull request's diff. + - name: Compare the public API with its record + id: record + if: ${{ !cancelled() && steps.compare.outcome == 'success' }} + run: > + bash .github/scripts/public_api_record.sh + "$RUNNER_TEMP/api-head/sdk-java.txt=sdk-java/api/sdk-java.txt" "$RUNNER_TEMP/api-head/sdk-java-ui.txt=sdk-java-ui/api/sdk-java-ui.txt" + + - name: Upload the new record + if: ${{ !cancelled() && steps.record.outcome == 'failure' }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: public-api-record + path: ${{ runner.temp }}/public-api-record + + comment: + name: Comment the public API changes + needs: api + # A fork's or Dependabot's pull request only gets a read-only token, so its report stays in the job summary. + if: >- + always() && needs.api.outputs.report != '' && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]' + runs-on: ubuntu-24.04 + permissions: + pull-requests: write + steps: + - name: Post or update the comment + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + REPORT: ${{ needs.api.outputs.report }} + run: | + id=$(gh api --paginate "repos/$REPO/issues/$PR/comments" --jq \ + '.[] | select(.user.login == "github-actions[bot]" and (.body | startswith(""))) | .id' | tail -n 1) + if [ -n "$id" ]; then + gh api --method PATCH "repos/$REPO/issues/comments/$id" -f "body=$REPORT" > /dev/null + elif ! grep -qx 'No public API change.' <<< "$REPORT"; then + gh api --method POST "repos/$REPO/issues/$PR/comments" -f "body=$REPORT" > /dev/null + fi diff --git a/sdk-java-ui/api/sdk-java-ui.txt b/sdk-java-ui/api/sdk-java-ui.txt new file mode 100644 index 00000000..e34128f7 --- /dev/null +++ b/sdk-java-ui/api/sdk-java-ui.txt @@ -0,0 +1,100 @@ +// Signature format: 4.0 +package ly.count.sdk.java.ui { + + public class ContentJsBridge { + method public void post(String!); + } + + public final class CountlyWebView { + method public static void disableContentZone(); + method public static void enableContentZone(); + method public static void enableContentZone(javafx.stage.Window!); + method public static boolean isShowingWidgetsWithinApp(); + method public static void presentFeedbackWidget(javafx.stage.Window!, ly.count.sdk.java.internal.CountlyFeedbackWidget!, Runnable!); + method public static void presentNPS(javafx.stage.Window!); + method public static void presentNPS(javafx.stage.Window!, String!); + method public static void presentNPS(javafx.stage.Window!, String!, Runnable!); + method public static void presentRating(javafx.stage.Window!); + method public static void presentRating(javafx.stage.Window!, String!); + method public static void presentRating(javafx.stage.Window!, String!, Runnable!); + method public static void presentSurvey(javafx.stage.Window!); + method public static void presentSurvey(javafx.stage.Window!, String!); + method public static void presentSurvey(javafx.stage.Window!, String!, Runnable!); + method public static void setOverlayCornerRadius(double); + method public static void setShowWidgetsWithinApp(boolean); + method public static void setWebViewDiagnosticsEnabled(boolean); + } + + public class FeedbackWidgetPresenter implements ly.count.sdk.java.ui.WidgetWebHost.Listener { + ctor public FeedbackWidgetPresenter(ly.count.sdk.java.ui.WidgetWebHost!, ly.count.sdk.java.internal.ModuleFeedback.Feedback!, Runnable!); + method public void onCardFollowing(int, int); + method public void onCardMeasured(int, int); + method public void onContentOverflow(int); + method public void onLoadFailed(); + method public void onNavigationStarting(String!); + method public void onPageLoaded(); + method public void onSizeNotReported(int, int); + method public void onWidgetMessage(String!); + method public void refreshPlacement(); + method public void start(ly.count.sdk.java.internal.CountlyFeedbackWidget!); + } + + public class JavaFxContentDisplay implements ly.count.sdk.java.internal.ContentDisplay { + ctor public JavaFxContentDisplay(); + ctor public JavaFxContentDisplay(javafx.stage.Window!); + method public ly.count.sdk.java.internal.ContentScreen! getScreen(); + method public void present(ly.count.sdk.java.internal.ContentData!, ly.count.sdk.java.internal.ContentCloseCallback!); + } + + public class WidgetJsBridge { + method public void cardChanged(String!); + method public void post(String!); + } + + public class WidgetMessageParser { + method public static ly.count.sdk.java.internal.WidgetAction! parse(String!); + } + + public class WidgetPlacement { + method public static ly.count.sdk.java.internal.ContentPlacement! following(ly.count.sdk.java.internal.ContentPlacement!, ly.count.sdk.java.ui.WidgetSurface!, ly.count.sdk.java.internal.ContentPlacement!, ly.count.sdk.java.ui.WidgetSurface!, ly.count.sdk.java.ui.WidgetSurface!); + method public static ly.count.sdk.java.internal.ContentPlacement! rescale(ly.count.sdk.java.internal.ContentPlacement!, ly.count.sdk.java.ui.WidgetSurface!, ly.count.sdk.java.ui.WidgetSurface!); + method public static ly.count.sdk.java.internal.ContentPlacement! resolve(ly.count.sdk.java.internal.ContentPlacement!, ly.count.sdk.java.ui.WidgetSurface!); + method public static ly.count.sdk.java.internal.ContentPlacement! resolve(ly.count.sdk.java.internal.WidgetAction!, ly.count.sdk.java.ui.WidgetSurface!); + } + + public class WidgetSurface { + ctor public WidgetSurface(int, int, int, int); + method public boolean isLandscape(); + method public boolean sameAs(ly.count.sdk.java.ui.WidgetSurface!); + method public boolean sameSizeAs(ly.count.sdk.java.ui.WidgetSurface!); + method public String toString(); + field public final int height; + field public final int width; + field public final int x; + field public final int y; + } + + public interface WidgetWebHost { + method public void closeHost(); + method public void followGeometry(ly.count.sdk.java.internal.ContentPlacement!); + method public ly.count.sdk.java.ui.WidgetSurface! getSurface(); + method public void navigate(String!); + method public void placeAndShow(ly.count.sdk.java.internal.ContentPlacement!); + method public void reportSurfaceSize(int, int); + method public void setListener(ly.count.sdk.java.ui.WidgetWebHost.Listener!); + method public void setSurface(ly.count.sdk.java.ui.WidgetSurface!); + } + + public static interface WidgetWebHost.Listener { + method public void onCardFollowing(int, int); + method public void onCardMeasured(int, int); + method public void onContentOverflow(int); + method public void onLoadFailed(); + method public void onNavigationStarting(String!); + method public void onPageLoaded(); + method public void onSizeNotReported(int, int); + method public void onWidgetMessage(String!); + } + +} + diff --git a/sdk-java/api/requests/consent.json b/sdk-java/api/requests/consent.json new file mode 100644 index 00000000..0b8e2bd7 --- /dev/null +++ b/sdk-java/api/requests/consent.json @@ -0,0 +1,65 @@ +[ + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "begin_session": "1", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "metrics": { + "_app_version": "", + "_device": "", + "_locale": "", + "_os": "", + "_os_version": "", + "_resolution": "" + }, + "sdk_name": "java-native", + "sdk_version": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-001", + "dow": "", + "events": [ + { + "count": 1, + "cvid": "", + "dow": "", + "hour": "", + "id": "", + "key": "consented", + "timestamp": "" + } + ], + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-001", + "dow": "", + "end_session": "1", + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "session_duration": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + } +] diff --git a/sdk-java/api/requests/crash.json b/sdk-java/api/requests/crash.json new file mode 100644 index 00000000..b1a2a71c --- /dev/null +++ b/sdk-java/api/requests/crash.json @@ -0,0 +1,40 @@ +[ + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "crash": { + "_app_version": "1.0", + "_bat": "", + "_cpu": "", + "_custom": { + "tier": "gold" + }, + "_device": "", + "_disk_current": "", + "_disk_total": "", + "_error": "", + "_logs": "breadcrumb one", + "_manufacture": "", + "_muted": "", + "_nonfatal": true, + "_online": "", + "_opengl": "", + "_orientation": "", + "_os": "", + "_os_version": "", + "_ram_current": "", + "_ram_total": "", + "_resolution": "", + "_run": "" + }, + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "timestamp": "", + "tz": "" + } + } +] diff --git a/sdk-java/api/requests/device-id.json b/sdk-java/api/requests/device-id.json new file mode 100644 index 00000000..a19eceab --- /dev/null +++ b/sdk-java/api/requests/device-id.json @@ -0,0 +1,142 @@ +[ + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "begin_session": "1", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "metrics": { + "_app_version": "", + "_device": "", + "_locale": "", + "_os": "", + "_os_version": "", + "_resolution": "" + }, + "sdk_name": "java-native", + "sdk_version": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-002", + "dow": "", + "hour": "", + "old_device_id": "parity-device-001", + "sdk_name": "java-native", + "sdk_version": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-002", + "dow": "", + "events": [ + { + "count": 1, + "cvid": "", + "dow": "", + "hour": "", + "id": "", + "key": "after_merge", + "timestamp": "" + } + ], + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-002", + "dow": "", + "end_session": "1", + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "session_duration": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "begin_session": "1", + "device_id": "parity-device-003", + "dow": "", + "hour": "", + "metrics": { + "_app_version": "", + "_device": "", + "_locale": "", + "_os": "", + "_os_version": "", + "_resolution": "" + }, + "sdk_name": "java-native", + "sdk_version": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-003", + "dow": "", + "end_session": "1", + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-003", + "dow": "", + "events": [ + { + "count": 1, + "cvid": "", + "dow": "", + "hour": "", + "id": "", + "key": "after_new_id", + "peid": "", + "timestamp": "" + } + ], + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "timestamp": "", + "tz": "" + } + } +] diff --git a/sdk-java/api/requests/parity.json b/sdk-java/api/requests/parity.json new file mode 100644 index 00000000..5afcf7f6 --- /dev/null +++ b/sdk-java/api/requests/parity.json @@ -0,0 +1,115 @@ +[ + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "begin_session": "1", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "metrics": { + "_app_version": "", + "_device": "", + "_locale": "", + "_os": "", + "_os_version": "", + "_resolution": "" + }, + "sdk_name": "java-native", + "sdk_version": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-001", + "dow": "", + "events": [ + { + "count": 2, + "cvid": "", + "dow": "", + "dur": 5, + "hour": "", + "id": "", + "key": "purchase", + "segmentation": { + "hashirama": -2147483647, + "itachi": 1.7976931348623157E+308, + "kakaski": 4.9E-324, + "nar": "uto", + "sakura": false, + "sasuke": 2147483647 + }, + "sum": 9.99, + "timestamp": "" + } + ], + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "timestamp": "", + "tz": "", + "user_details": { + "byear": 1980, + "custom": { + "age": 30, + "tier": "gold" + }, + "email": "john@example.com", + "gender": "M", + "name": "John Doe", + "organization": "Countly", + "phone": "1234567890", + "username": "john_d" + } + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "session_duration": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-001", + "dow": "", + "end_session": "1", + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + } +] diff --git a/sdk-java/api/requests/views.json b/sdk-java/api/requests/views.json new file mode 100644 index 00000000..51e9300c --- /dev/null +++ b/sdk-java/api/requests/views.json @@ -0,0 +1,104 @@ +[ + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "begin_session": "1", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "metrics": { + "_app_version": "", + "_device": "", + "_locale": "", + "_os": "", + "_os_version": "", + "_resolution": "" + }, + "sdk_name": "java-native", + "sdk_version": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-001", + "dow": "", + "end_session": "1", + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "session_id": "", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "device_id": "parity-device-001", + "dow": "", + "events": [ + { + "count": 1, + "dow": "", + "dur": "", + "hour": "", + "id": "", + "key": "[CLY]_view", + "pvid": "", + "segmentation": { + "name": "Home", + "segment": "", + "source": "scenario", + "start": "1", + "visit": "1" + }, + "sum": 0, + "timestamp": "" + }, + { + "count": 1, + "dow": "", + "dur": "", + "hour": "", + "id": "", + "key": "[CLY]_view", + "pvid": "", + "segmentation": { + "name": "Home", + "segment": "" + }, + "sum": 0, + "timestamp": "" + }, + { + "count": 1, + "dow": "", + "dur": "", + "hour": "", + "id": "", + "key": "[CLY]_view", + "pvid": "", + "segmentation": { + "name": "Settings", + "segment": "", + "visit": "1" + }, + "sum": 0, + "timestamp": "" + } + ], + "hour": "", + "sdk_name": "java-native", + "sdk_version": "", + "timestamp": "", + "tz": "" + } + } +] diff --git a/sdk-java/api/sdk-java.txt b/sdk-java/api/sdk-java.txt new file mode 100644 index 00000000..3a6c1a0e --- /dev/null +++ b/sdk-java/api/sdk-java.txt @@ -0,0 +1,1556 @@ +// Signature format: 4.0 +package ly.count.sdk.java { + + public class Config { + ctor @Deprecated public Config(String!, String!); + ctor public Config(String!, String!, File!); + method public ly.count.sdk.java.Config! addCertificatePin(String!); + method public ly.count.sdk.java.Config! addCustomNetworkRequestHeaders(Map!); + method public ly.count.sdk.java.Config! addPublicKeyPin(String!); + method @Deprecated public ly.count.sdk.java.Config! disableANRCrashReporting(); + method public ly.count.sdk.java.Config! disableAutoSendUserProperties(); + method public ly.count.sdk.java.Config! disableBackoffMechanism(); + method public ly.count.sdk.java.Config! disableFeatures(ly.count.sdk.java.Config.Feature!...!); + method public ly.count.sdk.java.Config! disableHealthCheck(); + method public ly.count.sdk.java.Config! disableLocation(); + method public ly.count.sdk.java.Config! disableSdkBehaviorSettingsUpdates(); + method @Deprecated public ly.count.sdk.java.Config! disableTestMode(); + method public ly.count.sdk.java.Config! disableUnhandledCrashReporting(); + method public ly.count.sdk.java.Config! disableUpdateRequests(); + method public ly.count.sdk.java.Config! enableBackendMode(); + method public ly.count.sdk.java.Config! enableFeatures(ly.count.sdk.java.Config.Feature!...!); + method public ly.count.sdk.java.Config! enableForcedHTTPPost(); + method public ly.count.sdk.java.Config! enableParameterTamperingProtection(String!); + method public ly.count.sdk.java.Config! enableRemoteConfigAutomaticTriggers(); + method public ly.count.sdk.java.Config! enableRemoteConfigValueCaching(); + method @Deprecated public ly.count.sdk.java.Config! enableTestMode(); + method @Deprecated public ly.count.sdk.java.Config! enableUsePOST(); + method public ly.count.sdk.java.Config! enrollABOnRCDownload(); + method @Deprecated public String! getApplicationName(); + method public String! getApplicationVersion(); + method public Set! getCertificatePins(); + method public String! getCrashProcessorClass(); + method @Deprecated public int getCrashReportingANRCheckingPeriod(); + method public String! getCustomDeviceId(); + method public Map! getCustomNetworkRequestHeaders(); + method public int getDeviceIdStrategy(); + method public ly.count.sdk.java.Config.DeviceIdStrategy! getDeviceIdStrategyEnum(); + method @Deprecated public int getEventsBufferSize(); + method public Set! getFeatures(); + method public int getFeaturesMap(); + method public ly.count.sdk.java.internal.LogCallback! getLogListener(); + method public ly.count.sdk.java.Config.LoggingLevel! getLoggingLevel(); + method @Deprecated public String! getLoggingTag(); + method @Deprecated public Class! getModuleOverride(int); + method @Deprecated public Class! getModuleOverride(ly.count.sdk.java.Config.Feature!); + method public int getNetworkConnectionTimeout(); + method public int getNetworkImportantRequestCooldown(); + method public int getNetworkReadTimeout(); + method public int getNetworkRequestCooldown(); + method public String! getParameterTamperingProtectionSalt(); + method public Set! getPublicKeyPins(); + method public int getRequestQueueMaxSize(); + method @Deprecated public String! getSdkName(); + method public String! getSdkPlatform(); + method @Deprecated public String! getSdkVersion(); + method public int getSendUpdateEachSeconds(); + method public String! getServerAppKey(); + method public URL! getServerURL(); + method public boolean isBackendModeEnabled(); + method @Deprecated public boolean isDeviceIdFallbackAllowed(); + method public boolean isFeatureEnabled(int); + method public boolean isFeatureEnabled(ly.count.sdk.java.Config.Feature!); + method public boolean isHTTPPostForced(); + method @Deprecated public boolean isTestModeEnabled(); + method @Deprecated protected ly.count.sdk.java.Config! overrideModule(Integer!, Class!); + method public ly.count.sdk.java.Config! remoteConfigRegisterGlobalCallback(ly.count.sdk.java.internal.RCDownloadCallback!); + method public boolean requiresConsent(); + method @Deprecated public ly.count.sdk.java.Config! setApplicationName(String!); + method public ly.count.sdk.java.Config! setApplicationVersion(String!); + method public ly.count.sdk.java.Config! setCrashProcessorClass(Class!); + method @Deprecated public ly.count.sdk.java.Config! setCrashReportingANRCheckingPeriod(int); + method public ly.count.sdk.java.Config! setCustomDeviceId(String!); + method @Deprecated public ly.count.sdk.java.Config! setDeviceIdFallbackAllowed(boolean); + method public ly.count.sdk.java.Config! setDeviceIdStrategy(ly.count.sdk.java.Config.DeviceIdStrategy!); + method public ly.count.sdk.java.Config! setDeviceIdStrategy(ly.count.sdk.java.Config.DeviceIdStrategy!, String!); + method public ly.count.sdk.java.Config! setEventQueueSizeToSend(int); + method @Deprecated public ly.count.sdk.java.Config! setEventsBufferSize(int); + method public ly.count.sdk.java.Config! setFeatures(ly.count.sdk.java.Config.Feature!...!); + method public ly.count.sdk.java.Config! setLocation(String!, String!, String!, String!); + method public ly.count.sdk.java.Config! setLogListener(ly.count.sdk.java.internal.LogCallback!); + method public ly.count.sdk.java.Config! setLoggingLevel(ly.count.sdk.java.Config.LoggingLevel!); + method @Deprecated public ly.count.sdk.java.Config! setLoggingTag(String!); + method public ly.count.sdk.java.Config! setMaxBreadcrumbCount(int); + method public ly.count.sdk.java.Config! setMaxKeyLength(int); + method public ly.count.sdk.java.Config! setMetricOverride(Map!); + method public ly.count.sdk.java.Config! setNetworkConnectTimeout(int); + method public ly.count.sdk.java.Config! setNetworkImportantRequestCooldown(int); + method public ly.count.sdk.java.Config! setNetworkReadTimeout(int); + method public ly.count.sdk.java.Config! setNetworkRequestCooldown(int); + method public ly.count.sdk.java.Config! setRequestQueueMaxSize(int); + method public ly.count.sdk.java.Config! setRequiresConsent(boolean); + method public ly.count.sdk.java.Config! setSdkBehaviorSettings(String!); + method @Deprecated public ly.count.sdk.java.Config! setSdkName(String!); + method public ly.count.sdk.java.Config! setSdkPlatform(String!); + method @Deprecated public ly.count.sdk.java.Config! setSdkVersion(String!); + method @Deprecated public ly.count.sdk.java.Config! setSendUpdateEachSeconds(int); + method public ly.count.sdk.java.Config! setUpdateSessionTimerDelay(int); + method @Deprecated public ly.count.sdk.java.Config! setUsePOST(boolean); + field protected String! applicationVersion; + field protected boolean autoSendUserProperties; + field protected boolean backoffMechanismEnabled; + field protected Set! certificatePins; + field protected String! city; + field protected ly.count.sdk.java.internal.Log! configLog; + field public ly.count.sdk.java.internal.ConfigContent! content; + field protected String! country; + field protected String! crashProcessorClass; + field protected String! customDeviceId; + field protected Map! customNetworkRequestHeaders; + field protected int deviceIdStrategy; + field protected boolean enableAutoEnrollFlag; + field protected boolean enableBackendMode; + field protected boolean enableRemoteConfigAutomaticDownloadTriggers; + field protected boolean enableRemoteConfigValueCaching; + field protected int eventQueueThreshold; + field protected int features; + field protected boolean forceHTTPPost; + field protected boolean healthCheckEnabled; + field protected String! ip; + field protected String! location; + field protected boolean locationEnabled; + field protected ly.count.sdk.java.internal.LogCallback! logListener; + field protected ly.count.sdk.java.Config.LoggingLevel! loggingLevel; + field protected int maxBreadcrumbCount; + field protected int maxKeyLength; + field protected final Map! metricOverride; + field protected Map!>! moduleOverrides; + field protected int networkConnectionTimeout; + field protected int networkImportantRequestCooldown; + field protected int networkReadTimeout; + field protected int networkRequestCooldown; + field protected Set! publicKeyPins; + field protected List! remoteConfigGlobalCallbacks; + field protected int requestQueueMaxSize; + field protected boolean requiresConsent; + field protected String! salt; + field protected String! sdkBehaviorSettings; + field protected boolean sdkBehaviorSettingsRequestsDisabled; + field protected String! sdkName; + field protected String! sdkPlatform; + field protected File! sdkStorageRootDirectory; + field protected String! sdkVersion; + field protected int sendUpdateEachSeconds; + field protected volatile String! serverAppKey; + field protected volatile URL! serverURL; + field protected boolean unhandledCrashReportingEnabled; + field public ly.count.sdk.java.internal.ConfigViews! views; + } + + public static final class Config.DID { + ctor public Config.DID(int, String!); + method public boolean equals(Object?); + method public int hashCode(); + method public String toString(); + field public static final int STRATEGY_CUSTOM = 10; // 0xa + field public static final int STRATEGY_UUID = 0; // 0x0 + field public String! id; + field public int strategy; + } + + public enum Config.DeviceIdStrategy { + method public static ly.count.sdk.java.Config.DeviceIdStrategy! fromIndex(int); + method public int getIndex(); + enum_constant public static final ly.count.sdk.java.Config.DeviceIdStrategy CUSTOM_ID; + enum_constant public static final ly.count.sdk.java.Config.DeviceIdStrategy UUID; + } + + public enum Config.Feature { + method public static ly.count.sdk.java.Config.Feature! byIndex(int); + method public int getIndex(); + enum_constant public static final ly.count.sdk.java.Config.Feature Content; + enum_constant public static final ly.count.sdk.java.Config.Feature CrashReporting; + enum_constant public static final ly.count.sdk.java.Config.Feature Events; + enum_constant public static final ly.count.sdk.java.Config.Feature Feedback; + enum_constant public static final ly.count.sdk.java.Config.Feature Location; + enum_constant public static final ly.count.sdk.java.Config.Feature RemoteConfig; + enum_constant public static final ly.count.sdk.java.Config.Feature Sessions; + enum_constant public static final ly.count.sdk.java.Config.Feature UserProfiles; + enum_constant public static final ly.count.sdk.java.Config.Feature Views; + } + + public enum Config.LoggingLevel { + method public int getLevel(); + method public boolean prints(ly.count.sdk.java.Config.LoggingLevel!); + enum_constant public static final ly.count.sdk.java.Config.LoggingLevel DEBUG; + enum_constant public static final ly.count.sdk.java.Config.LoggingLevel ERROR; + enum_constant public static final ly.count.sdk.java.Config.LoggingLevel INFO; + enum_constant public static final ly.count.sdk.java.Config.LoggingLevel OFF; + enum_constant public static final ly.count.sdk.java.Config.LoggingLevel VERBOSE; + enum_constant public static final ly.count.sdk.java.Config.LoggingLevel WARN; + } + + public class Countly implements ly.count.sdk.java.Usage { + ctor public Countly(); + ctor protected Countly(ly.count.sdk.java.internal.SDKCore!, ly.count.sdk.java.internal.Log!); + method public ly.count.sdk.java.Usage! addCrashReport(Throwable!, boolean); + method public ly.count.sdk.java.Usage! addCrashReport(Throwable!, boolean, String!, Map!, String!...!); + method public ly.count.sdk.java.Usage! addLocation(double, double); + method public ly.count.sdk.java.Usage! addParam(String!, Object!); + method public static ly.count.sdk.java.Usage! api(); + method public void attemptToSendStoredRequests(); + method public ly.count.sdk.java.internal.ModuleBackendMode.BackendMode! backendM(); + method @Deprecated public static ly.count.sdk.java.internal.ModuleBackendMode.BackendMode! backendMode(); + method public void changeAppKey(String!); + method @Deprecated public ly.count.sdk.java.Usage! changeDeviceIdWithMerge(String!); + method @Deprecated public ly.count.sdk.java.Usage! changeDeviceIdWithoutMerge(String!); + method public void changeServerUrl(String!); + method public ly.count.sdk.java.internal.ModuleContent.Content! content(); + method public ly.count.sdk.java.internal.ModuleCrashes.Crashes! crashes(); + method public ly.count.sdk.java.internal.ModuleDeviceIdCore.DeviceId! deviceId(); + method @Deprecated public ly.count.sdk.java.Event! event(String!); + method public ly.count.sdk.java.internal.ModuleEvents.Events! events(); + method public ly.count.sdk.java.internal.ModuleFeedback.Feedback! feedback(); + method public void flushQueues(); + method @Deprecated public String! getDeviceId(); + method public ly.count.sdk.java.internal.DeviceIdType! getDeviceIdType(); + method public void halt(); + method @Deprecated public static void init(File!, ly.count.sdk.java.Config!); + method public void init(ly.count.sdk.java.Config!); + method public static ly.count.sdk.java.Countly! instance(); + method public static boolean isInitialized(); + method public static boolean isTracking(ly.count.sdk.java.Config.Feature!); + method public ly.count.sdk.java.internal.ModuleLocation.Location! location(); + method public ly.count.sdk.java.Usage! login(String!); + method public ly.count.sdk.java.Usage! logout(); + method public static void onConsent(ly.count.sdk.java.Config.Feature!...!); + method public static void onConsentRemoval(ly.count.sdk.java.Config.Feature!...!); + method public ly.count.sdk.java.internal.ModuleRemoteConfig.RemoteConfig! remoteConfig(); + method public void removeDifferentAppKeysFromQueue(); + method public void replaceAllAppKeysInQueueWithCurrentAppKey(); + method public static ly.count.sdk.java.Session! session(); + method public void stop(); + method @Deprecated public static void stop(boolean); + method @Deprecated public ly.count.sdk.java.Event! timedEvent(String!); + method public ly.count.sdk.java.User! user(); + method public ly.count.sdk.java.internal.ModuleUserProfile.UserProfile! userProfile(); + method @Deprecated public ly.count.sdk.java.View! view(String!); + method @Deprecated public ly.count.sdk.java.View! view(String!, boolean); + method public ly.count.sdk.java.internal.ModuleViews.Views! views(); + field protected ly.count.sdk.java.internal.Log! L; + field public static final ly.count.sdk.java.internal.Device! device; + field protected ly.count.sdk.java.internal.SDKCore! sdk; + } + + public interface Crash { + method public ly.count.sdk.java.Crash! addException(Exception!); + method public ly.count.sdk.java.Crash! addThrowable(Throwable!); + method public ly.count.sdk.java.Crash! addTraces(Thread!, Map!); + method public List! getLogs(); + method public String! getName(); + method public Map! getSegments(); + method public Throwable! getThrowable(); + method public Map! getTraces(); + method public boolean isFatal(); + method public ly.count.sdk.java.Crash! setFatal(boolean); + method public ly.count.sdk.java.Crash! setLogs(String![]!); + method public ly.count.sdk.java.Crash! setName(String!); + method public ly.count.sdk.java.Crash! setSegments(Map!); + } + + public interface CrashProcessor { + method public ly.count.sdk.java.Crash! process(ly.count.sdk.java.Crash!); + } + + @Deprecated public interface Event { + method @Deprecated public ly.count.sdk.java.Event! addSegment(String, String); + method @Deprecated public ly.count.sdk.java.Event! addSegments(String!...); + method @Deprecated public void endAndRecord(); + method @Deprecated public boolean isInvalid(); + method @Deprecated public void record(); + method @Deprecated public ly.count.sdk.java.Event! setCount(int); + method @Deprecated public ly.count.sdk.java.Event! setDuration(double); + method @Deprecated public ly.count.sdk.java.Event! setSegmentation(Map); + method @Deprecated public ly.count.sdk.java.Event! setSum(double); + } + + public class PredefinedUserPropertyKeys { + ctor public PredefinedUserPropertyKeys(); + field public static final String BIRTH_YEAR = "byear"; + field public static final String EMAIL = "email"; + field public static final String GENDER = "gender"; + field public static final String NAME = "name"; + field public static final String ORGANIZATION = "organization"; + field public static final String PHONE = "phone"; + field public static final String PICTURE = "picture"; + field public static final String PICTURE_PATH = "picturePath"; + field public static final String USERNAME = "username"; + } + + public interface Session extends ly.count.sdk.java.Usage { + method public ly.count.sdk.java.Session! begin(); + method public void end(); + method public Long! getBegan(); + method public Long! getEnded(); + method public Long! getId(); + method public boolean isActive(); + method public ly.count.sdk.java.Session! update(); + } + + public interface Usage { + method @Deprecated public ly.count.sdk.java.Usage! addCrashReport(Throwable!, boolean); + method @Deprecated public ly.count.sdk.java.Usage! addCrashReport(Throwable!, boolean, String!, Map!, String!...!); + method @Deprecated public ly.count.sdk.java.Usage! addLocation(double, double); + method public ly.count.sdk.java.Usage! addParam(String!, Object!); + method @Deprecated public ly.count.sdk.java.Usage! changeDeviceIdWithMerge(String!); + method @Deprecated public ly.count.sdk.java.Usage! changeDeviceIdWithoutMerge(String!); + method @Deprecated public ly.count.sdk.java.Event! event(String!); + method @Deprecated public String! getDeviceId(); + method @Deprecated public ly.count.sdk.java.Usage! login(String!); + method @Deprecated public ly.count.sdk.java.Usage! logout(); + method @Deprecated public ly.count.sdk.java.Event! timedEvent(String!); + method public ly.count.sdk.java.User! user(); + method @Deprecated public ly.count.sdk.java.View! view(String!); + method @Deprecated public ly.count.sdk.java.View! view(String!, boolean); + } + + public abstract class User { + ctor public User(); + method public abstract Integer! birthyear(); + method public abstract String! city(); + method public abstract String! country(); + method public abstract Map! custom(); + method public abstract ly.count.sdk.java.UserEditor! edit(); + method public abstract String! email(); + method public abstract ly.count.sdk.java.User.Gender! gender(); + method public abstract String! locale(); + method public abstract String! location(); + method public abstract String! name(); + method public abstract String! org(); + method public abstract String! phone(); + method public abstract byte[]! picture(); + method public abstract String! picturePath(); + method public abstract String! username(); + } + + public enum User.Gender { + method public static ly.count.sdk.java.User.Gender! fromString(String!); + method public String toString(); + enum_constant public static final ly.count.sdk.java.User.Gender FEMALE; + enum_constant public static final ly.count.sdk.java.User.Gender MALE; + } + + @Deprecated public interface UserEditor { + method @Deprecated public ly.count.sdk.java.UserEditor! addToCohort(String!); + method @Deprecated public ly.count.sdk.java.User! commit(); + method @Deprecated public ly.count.sdk.java.UserEditor! inc(String!, int); + method @Deprecated public ly.count.sdk.java.UserEditor! max(String!, double); + method @Deprecated public ly.count.sdk.java.UserEditor! min(String!, double); + method @Deprecated public ly.count.sdk.java.UserEditor! mul(String!, double); + method @Deprecated public ly.count.sdk.java.UserEditor! optOutFromLocationServices(); + method @Deprecated public ly.count.sdk.java.UserEditor! pull(String!, Object!); + method @Deprecated public ly.count.sdk.java.UserEditor! push(String!, Object!); + method @Deprecated public ly.count.sdk.java.UserEditor! pushUnique(String!, Object!); + method @Deprecated public ly.count.sdk.java.UserEditor! removeFromCohort(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! set(String!, Object!); + method @Deprecated public ly.count.sdk.java.UserEditor! setBirthyear(int); + method @Deprecated public ly.count.sdk.java.UserEditor! setBirthyear(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! setCity(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! setCountry(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! setCustom(String!, Object!); + method @Deprecated public ly.count.sdk.java.UserEditor! setEmail(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! setGender(Object!); + method @Deprecated public ly.count.sdk.java.UserEditor! setLocale(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! setLocation(double, double); + method @Deprecated public ly.count.sdk.java.UserEditor! setLocation(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! setName(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! setOnce(String!, Object!); + method @Deprecated public ly.count.sdk.java.UserEditor! setOrg(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! setPhone(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! setPicture(byte[]!); + method @Deprecated public ly.count.sdk.java.UserEditor! setPicturePath(String!); + method @Deprecated public ly.count.sdk.java.UserEditor! setUsername(String!); + } + + public interface View { + method @Deprecated public void start(boolean); + method @Deprecated public void stop(boolean); + } + +} + +package ly.count.sdk.java.internal { + + public interface Byteable { + method public boolean restore(byte[]!, ly.count.sdk.java.internal.Log!); + method public byte[]! store(ly.count.sdk.java.internal.Log!); + } + + public interface CallbackOnFinish { + method public void onFinished(T, String!); + } + + public class ConfigContent { + ctor public ConfigContent(ly.count.sdk.java.Config!); + method public ly.count.sdk.java.Config! setContentUrlHandler(ly.count.sdk.java.internal.ContentUrlHandler!); + method public ly.count.sdk.java.Config! setGlobalContentCallback(ly.count.sdk.java.internal.ContentCallback!); + method public ly.count.sdk.java.Config! setZoneTimerInterval(int); + field protected ly.count.sdk.java.internal.ContentUrlHandler! contentUrlHandler; + field protected ly.count.sdk.java.internal.ContentCallback! globalContentCallback; + field protected int zoneTimerInterval; + } + + public class ConfigViews { + ctor public ConfigViews(ly.count.sdk.java.Config!); + method public ly.count.sdk.java.Config! setGlobalViewSegmentation(Map!); + field protected Map! globalViewSegmentation; + } + + public interface ConfigurationProvider { + method public boolean getAutomaticCrashReportingEnabled(); + method public boolean getAutomaticSessionTrackingEnabled(); + method public boolean getAutomaticViewTrackingEnabled(); + method public int getBOMAcceptedTimeoutSeconds(); + method public int getBOMDuration(); + method public boolean getBOMEnabled(); + method public double getBOMRQPercentage(); + method public int getBOMRequestAge(); + method public boolean getConsentRequired(); + method public boolean getContentZoneEnabled(); + method public int getContentZoneTimerInterval(); + method public boolean getCrashReportingEnabled(); + method public boolean getCustomEventTrackingEnabled(); + method public ly.count.sdk.java.internal.ConfigurationProvider.FilterList!> getEventFilterList(); + method public int getEventQueueSizeThreshold(); + method public ly.count.sdk.java.internal.ConfigurationProvider.FilterList!>!> getEventSegmentationFilterList(); + method public Set getJourneyTriggerEvents(); + method public Set getJourneyTriggerViews(); + method public boolean getLocationTrackingEnabled(); + method public int getLogGatheringBatchSize(); + method public String? getLogGatheringId(); + method public String getLogGatheringLevels(); + method public ly.count.sdk.java.internal.ConfigurationProvider.LogGatheringState getLogGatheringState(); + method public boolean getLoggingEnabled(); + method public int getMaxBreadcrumbCount(); + method public int getMaxKeyLength(); + method public int getMaxSegmentationValues(); + method public int getMaxStackTraceLineLength(); + method public int getMaxStackTraceLinesPerThread(); + method public int getMaxValueSize(); + method public boolean getNetworkingEnabled(); + method public boolean getRefreshContentZoneEnabled(); + method public int getRequestDropAgeHours(); + method public int getRequestQueueMaxSize(); + method public ly.count.sdk.java.internal.ConfigurationProvider.FilterList!> getSegmentationFilterList(); + method public int getServerConfigUpdateInterval(); + method public boolean getSessionTrackingEnabled(); + method public int getSessionUpdateInterval(); + method public boolean getTrackingEnabled(); + method public int getUserPropertyCacheLimit(); + method public ly.count.sdk.java.internal.ConfigurationProvider.FilterList!> getUserPropertyFilterList(); + method public boolean getViewTrackingEnabled(); + method public boolean isRequestQueueMaxSizeFromBehaviorSettings(); + } + + public static final class ConfigurationProvider.FilterList { + method public T getFilterList(); + method public boolean isWhitelist(); + method public String toString(); + } + + public enum ConfigurationProvider.LogGatheringState { + enum_constant public static final ly.count.sdk.java.internal.ConfigurationProvider.LogGatheringState GATHERING; + enum_constant public static final ly.count.sdk.java.internal.ConfigurationProvider.LogGatheringState NOT_GATHERING; + enum_constant public static final ly.count.sdk.java.internal.ConfigurationProvider.LogGatheringState UNDECIDED; + } + + public interface ContentCallback { + method public void onContentCallback(ly.count.sdk.java.internal.ContentStatus!, Map!); + } + + public interface ContentCloseCallback { + method public void onClosed(Map!); + } + + public class ContentData { + ctor public ContentData(String!, ly.count.sdk.java.internal.ContentPlacement!, ly.count.sdk.java.internal.ContentPlacement!); + method public ly.count.sdk.java.internal.ContentPlacement! placementFor(boolean); + method public String toString(); + field public final ly.count.sdk.java.internal.ContentPlacement! landscape; + field public final ly.count.sdk.java.internal.ContentPlacement! portrait; + field public final String! url; + } + + public interface ContentDisplay { + method public ly.count.sdk.java.internal.ContentScreen! getScreen(); + method public void present(ly.count.sdk.java.internal.ContentData!, ly.count.sdk.java.internal.ContentCloseCallback!); + } + + public class ContentPlacement { + ctor public ContentPlacement(int, int, int, int); + method public String toString(); + field public final int height; + field public final int width; + field public final int x; + field public final int y; + } + + public class ContentScreen { + ctor public ContentScreen(int, int); + method public String toString(); + field public final int height; + field public final int width; + } + + public enum ContentStatus { + enum_constant public static final ly.count.sdk.java.internal.ContentStatus CLOSED; + enum_constant public static final ly.count.sdk.java.internal.ContentStatus COMPLETED; + } + + public interface ContentUrlHandler { + method public boolean onContentUrl(String!); + } + + public enum CoreFeature { + method public ly.count.sdk.java.internal.ModuleBaseCreator! getCreator(); + method public int getIndex(); + enum_constant public static final ly.count.sdk.java.internal.CoreFeature BackendMode; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature Content; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature CrashReporting; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature DeviceId; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature Events; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature Feedback; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature Location; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature Logs; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature RemoteConfig; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature Requests; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature Sessions; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature TestDummy; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature UserProfiles; + enum_constant public static final ly.count.sdk.java.internal.CoreFeature Views; + } + + public class CountlyFeedbackWidget { + ctor public CountlyFeedbackWidget(); + method public boolean equals(Object?); + method public int hashCode(); + field public String! name; + field public String! position; + field public String![]! tags; + field public ly.count.sdk.java.internal.FeedbackWidgetType! type; + field public String! widgetId; + } + + public class CountlyTimer { + ctor protected CountlyTimer(ly.count.sdk.java.internal.Log!); + method protected void startTimer(long, long, Runnable!); + method protected void startTimer(long, Runnable!); + method protected void stopTimer(); + method protected void stopTimer(boolean); + field protected static int TIMER_DELAY_MS; + } + + public class CrashImpl implements ly.count.sdk.java.Crash ly.count.sdk.java.internal.Storable { + ctor protected CrashImpl(Long!, ly.count.sdk.java.internal.Log!); + ctor protected CrashImpl(ly.count.sdk.java.internal.Log!); + method protected ly.count.sdk.java.internal.CrashImpl! add(String!, Object!); + method public ly.count.sdk.java.internal.CrashImpl! addException(Exception!); + method protected ly.count.sdk.java.internal.CrashImpl! addSegments(Map!); + method public ly.count.sdk.java.internal.CrashImpl! addThrowable(Throwable!); + method public ly.count.sdk.java.internal.CrashImpl! addTraces(Thread!, Map!); + method public org.json.JSONObject! getData(); + method public String! getJSON(); + method public List! getLogs(); + method public String! getName(); + method public Map! getSegments(); + method public static String! getStoragePrefix(); + method public Throwable! getThrowable(); + method public Map! getTraces(); + method public boolean isFatal(); + method public ly.count.sdk.java.internal.CrashImpl! putMetrics(Long!); + method public boolean restore(byte[]!, ly.count.sdk.java.internal.Log!); + method public ly.count.sdk.java.internal.CrashImpl! setFatal(boolean); + method public void setId(Long!); + method public ly.count.sdk.java.internal.CrashImpl! setLogs(String![]!); + method public ly.count.sdk.java.internal.CrashImpl! setName(String!); + method public ly.count.sdk.java.internal.CrashImpl! setSegments(Map!); + method public Long! storageId(); + method public String! storagePrefix(); + method public byte[]! store(ly.count.sdk.java.internal.Log!); + method public String toString(); + } + + public class DefaultNetworking implements ly.count.sdk.java.internal.Networking { + ctor public DefaultNetworking(); + method public boolean check(ly.count.sdk.java.internal.InternalConfig!); + method public ly.count.sdk.java.internal.Transport! getTransport(); + method public void init(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.IStorageForRequestQueue!); + method public boolean isSending(); + method public void stop(ly.count.sdk.java.internal.InternalConfig!); + method protected ly.count.sdk.java.internal.Tasks.Task! submit(ly.count.sdk.java.internal.InternalConfig!, boolean); + } + + public class Device { + ctor protected Device(); + method public ly.count.sdk.java.internal.Params! buildMetrics(); + method public String! getAppVersion(); + method public Float! getBatteryLevel(); + method public String! getCpu(); + method public String! getDevice(); + method public Long! getDiskAvailable(); + method public Long! getDiskTotal(); + method public String! getLocale(); + method public String! getManufacturer(); + method public String! getOS(); + method public String! getOSVersion(); + method public String! getOpenGL(); + method public String! getOrientation(); + method public Long! getRAMAvailable(); + method public Long! getRAMTotal(); + method public String! getResolution(); + method public int getTimezoneOffset(); + method public Boolean! isMuted(); + method public Boolean! isOnline(); + method public ly.count.sdk.java.internal.Device! setAppVersion(String!); + method public ly.count.sdk.java.internal.Device! setBatteryLevel(Float!); + method public ly.count.sdk.java.internal.Device! setCpu(String!); + method public ly.count.sdk.java.internal.Device! setDevice(String!); + method public void setLog(ly.count.sdk.java.internal.Log!); + method public ly.count.sdk.java.internal.Device! setManufacturer(String!); + method public ly.count.sdk.java.internal.Device setMetricOverride(Map?); + method public ly.count.sdk.java.internal.Device! setMuted(Boolean!); + method public ly.count.sdk.java.internal.Device! setOnline(Boolean!); + method public ly.count.sdk.java.internal.Device! setOpenGL(String!); + method public ly.count.sdk.java.internal.Device! setOrientation(String!); + method public ly.count.sdk.java.internal.Device! setResolution(String!); + field protected static final Long BYTES_IN_MB; + field protected static final Double MS_IN_SECOND; + field protected static final Double NS_IN_MS; + field protected static final Double NS_IN_SECOND; + field public static ly.count.sdk.java.internal.Device! dev; + } + + public interface DeviceIdGenerator { + method public String! generate(ly.count.sdk.java.internal.InternalConfig!); + } + + public enum DeviceIdType { + method public static ly.count.sdk.java.internal.DeviceIdType! fromInt(int, ly.count.sdk.java.internal.Log!); + method public static int toInt(String!); + enum_constant public static final ly.count.sdk.java.internal.DeviceIdType DEVELOPER_SUPPLIED; + enum_constant public static final ly.count.sdk.java.internal.DeviceIdType SDK_GENERATED; + field public final int index; + } + + public class EventQueue { + ctor protected EventQueue(); + ctor protected EventQueue(ly.count.sdk.java.internal.Log, int); + method public void clear(); + method protected int eqSize(); + method protected List! getEQ(); + field protected final Object! lockEQ; + } + + public class FeedbackWidgetSelector { + method public static ly.count.sdk.java.internal.CountlyFeedbackWidget! select(List!, ly.count.sdk.java.internal.FeedbackWidgetType!, String!); + } + + public enum FeedbackWidgetType { + enum_constant public static final ly.count.sdk.java.internal.FeedbackWidgetType nps; + enum_constant public static final ly.count.sdk.java.internal.FeedbackWidgetType rating; + enum_constant public static final ly.count.sdk.java.internal.FeedbackWidgetType survey; + } + + public class HealthCheckCounter { + ctor public HealthCheckCounter(ly.count.sdk.java.internal.StorageProvider, ly.count.sdk.java.internal.Log, Predicate); + } + + public interface IStorageForRequestQueue { + method public ly.count.sdk.java.internal.Request! getNextRequest(); + method public Integer! remaningRequests(); + method public Boolean! removeRequest(ly.count.sdk.java.internal.Request!); + } + + public interface IdGenerator { + method public String generateId(); + } + + public interface ImmediateRequestGenerator { + method public ly.count.sdk.java.internal.ImmediateRequestI! createImmediateRequestMaker(); + } + + public interface ImmediateRequestI { + method public void doWork(String!, String!, ly.count.sdk.java.internal.Transport!, boolean, boolean, ly.count.sdk.java.internal.ImmediateRequestMaker.InternalImmediateRequestCallback!, ly.count.sdk.java.internal.Log!); + } + + public class InternalConfig extends ly.count.sdk.java.Config { + ctor public InternalConfig(); + ctor public InternalConfig(ly.count.sdk.java.Config!); + ctor public InternalConfig(String!, String!); + method public ly.count.sdk.java.Config.DID! getDeviceId(); + method public int getFeatures1(); + method public Map! getMetricOverride(); + method public Set! getModuleOverrides(); + method public boolean getNetworkingEnabled(); + method public List! getRemoteConfigGlobalCallbackList(); + method protected String! getSDKNameInternal(); + method public String! getSdkBehaviorSettings(); + method public static String! getStoragePrefix(); + method public boolean isAutoEnrollFlagEnabled(); + method public boolean isDefaultNetworking(); + method public boolean isRemoteConfigAutomaticDownloadTriggersEnabled(); + method public boolean isRemoteConfigValueCachingEnabled(); + method public boolean isSdkBehaviorSettingsRequestsDisabled(); + method public boolean isTemporaryIdEnabled(); + method public boolean removeDeviceId(ly.count.sdk.java.Config.DID!); + method public boolean removeDeviceId(String!); + method public void setDefaultNetworking(boolean); + method public ly.count.sdk.java.Config.DID! setDeviceId(ly.count.sdk.java.Config.DID!); + method public void setFrom(ly.count.sdk.java.Config!); + method public void setLogger(ly.count.sdk.java.internal.Log!); + method public static Long! storageId(); + field protected ly.count.sdk.java.internal.IdGenerator! eventIdGenerator; + field public ly.count.sdk.java.internal.SDKCore! sdk; + field public ly.count.sdk.java.internal.StorageProvider! storageProvider; + field protected ly.count.sdk.java.internal.IdGenerator! viewIdGenerator; + field protected ly.count.sdk.java.internal.ViewIdProvider! viewIdProvider; + } + + public interface JSONable { + method public String! toJSON(ly.count.sdk.java.internal.Log!); + } + + public class JsonFileStorage { + ctor public JsonFileStorage(File, ly.count.sdk.java.internal.Log); + method public void add(String, Object); + method public void addAndSave(String, Object); + method public void clear(); + method public void clearAndSave(); + method public void delete(String); + method public void deleteAndSave(String); + method public Object! get(String); + method public int getInt(String, int); + method public org.json.JSONObject! getJsonObj(String, org.json.JSONObject!); + method public String! getString(String); + method public String! getString(String, String!); + method public void save(); + method public int size(); + } + + public class Log { + ctor public Log(ly.count.sdk.java.Config.LoggingLevel!, ly.count.sdk.java.internal.LogCallback!); + method public void d(String!); + method public void e(String!); + method public void i(String!); + method public void recordLogCode(String); + method public void recordLogCode(String, int); + method public void recordLogCode(String, String); + method public static void recordLogCodeWhileNotInitialized(String); + method public void recordUsage(String, String); + method public void v(String!); + method public void w(String!); + } + + public interface LogCallback { + method public void LogHappened(String!, ly.count.sdk.java.Config.LoggingLevel!); + } + + public class MigrationHelper { + ctor protected MigrationHelper(ly.count.sdk.java.internal.Log!); + method protected void applyMigrations(Map!); + method protected boolean migration_DeleteConfigFile_01(Map!); + method protected boolean migration_UserImplFile_02(Map!); + method protected void setupMigrations(ly.count.sdk.java.internal.StorageProvider!); + field protected int currentDataModelVersion; + field protected ly.count.sdk.java.internal.Log! logger; + } + + public class ModuleBackendMode extends ly.count.sdk.java.internal.ModuleBase { + ctor public ModuleBackendMode(); + method public void disableModule(); + method public void recordExceptionInternal(String!, String!, String!, Map!, Map!, Long!); + method protected Map! removeInvalidDataFromSegments(Map!); + field protected boolean disabledModule; + field protected int eventQSize; + field protected final Map! eventQueues; + field protected ly.count.sdk.java.internal.InternalConfig! internalConfig; + } + + public class ModuleBackendMode.BackendMode { + ctor public ModuleBackendMode.BackendMode(); + method protected ly.count.sdk.java.internal.ModuleBase! getModule(); + method public int getQueueSize(); + method public void recordDirectRequest(String!, Map!, Long!); + method public void recordEvent(String!, String!, int, Double!, Double!, Map!, Long!); + method public void recordException(String!, String!, String!, Map!, Map!, Long!); + method public void recordException(String!, Throwable!, Map!, Map!, Long!); + method public void recordUserProperties(String!, Map!, Long!); + method public void recordView(String!, String!, Map!, Long!); + method public void sessionBegin(String!, Map!, Map!, Long!); + method public void sessionEnd(String!, double, Long!); + method public void sessionUpdate(String!, double, Long!); + } + + public abstract class ModuleBase { + ctor public ModuleBase(); + method protected void deviceIdChanged(String!, boolean); + method public void init(ly.count.sdk.java.internal.InternalConfig!); + method protected void initFinished(ly.count.sdk.java.internal.InternalConfig!); + method public boolean isActive(); + method public Boolean! onRequest(ly.count.sdk.java.internal.Request!); + method public void onRequestCompleted(ly.count.sdk.java.internal.Request!, String!, int); + method protected void onSdkConfigurationChanged(ly.count.sdk.java.internal.InternalConfig!); + method public void onSessionBegan(ly.count.sdk.java.Session!, ly.count.sdk.java.internal.InternalConfig!); + method public void onSessionEnded(ly.count.sdk.java.Session!, ly.count.sdk.java.internal.InternalConfig!); + method protected void onTimer(); + method public void setActive(boolean); + method public void stop(ly.count.sdk.java.internal.InternalConfig!, boolean); + field protected ly.count.sdk.java.internal.Log! L; + } + + public interface ModuleBaseCreator { + method public ly.count.sdk.java.internal.ModuleBase! create(); + } + + public class ModuleConfiguration extends ly.count.sdk.java.internal.ModuleBase implements ly.count.sdk.java.internal.ConfigurationProvider { + ctor public ModuleConfiguration(); + method public boolean getAutomaticCrashReportingEnabled(); + method public boolean getAutomaticSessionTrackingEnabled(); + method public boolean getAutomaticViewTrackingEnabled(); + method public int getBOMAcceptedTimeoutSeconds(); + method public int getBOMDuration(); + method public boolean getBOMEnabled(); + method public double getBOMRQPercentage(); + method public int getBOMRequestAge(); + method public boolean getConsentRequired(); + method public boolean getContentZoneEnabled(); + method public int getContentZoneTimerInterval(); + method public boolean getCrashReportingEnabled(); + method public boolean getCustomEventTrackingEnabled(); + method public ly.count.sdk.java.internal.ConfigurationProvider.FilterList!> getEventFilterList(); + method public int getEventQueueSizeThreshold(); + method public ly.count.sdk.java.internal.ConfigurationProvider.FilterList!>!> getEventSegmentationFilterList(); + method public Set getJourneyTriggerEvents(); + method public Set getJourneyTriggerViews(); + method public boolean getLocationTrackingEnabled(); + method public int getLogGatheringBatchSize(); + method public String? getLogGatheringId(); + method public String getLogGatheringLevels(); + method public ly.count.sdk.java.internal.ConfigurationProvider.LogGatheringState getLogGatheringState(); + method public boolean getLoggingEnabled(); + method public int getMaxBreadcrumbCount(); + method public int getMaxKeyLength(); + method public int getMaxSegmentationValues(); + method public int getMaxStackTraceLineLength(); + method public int getMaxStackTraceLinesPerThread(); + method public int getMaxValueSize(); + method public boolean getNetworkingEnabled(); + method public boolean getRefreshContentZoneEnabled(); + method public int getRequestDropAgeHours(); + method public int getRequestQueueMaxSize(); + method public ly.count.sdk.java.internal.ConfigurationProvider.FilterList!> getSegmentationFilterList(); + method public int getServerConfigUpdateInterval(); + method public boolean getSessionTrackingEnabled(); + method public int getSessionUpdateInterval(); + method public boolean getTrackingEnabled(); + method public int getUserPropertyCacheLimit(); + method public ly.count.sdk.java.internal.ConfigurationProvider.FilterList!> getUserPropertyFilterList(); + method public boolean getViewTrackingEnabled(); + method public void initFinished(ly.count.sdk.java.internal.InternalConfig); + method public boolean isRequestQueueMaxSizeFromBehaviorSettings(); + } + + public class ModuleContent extends ly.count.sdk.java.internal.ModuleBase { + } + + public class ModuleContent.Content { + ctor public ModuleContent.Content(); + method public void enterContentZone(); + method public void exitContentZone(); + method public void previewContent(String?); + method public void recordContentEvents(String?); + method public void refreshContentZone(); + method public void setContentDisplay(ly.count.sdk.java.internal.ContentDisplay?); + } + + public class ModuleCrashes extends ly.count.sdk.java.internal.ModuleBase { + ctor public ModuleCrashes(); + method protected void addBreadcrumbInternal(String!); + method public void initFinished(ly.count.sdk.java.internal.InternalConfig!); + method public ly.count.sdk.java.internal.CrashImpl! onCrash(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.CrashImpl!); + method protected boolean recordExceptionInternal(Throwable!, boolean, Map!, String!); + field protected ly.count.sdk.java.internal.InternalConfig! config; + field protected ly.count.sdk.java.CrashProcessor! crashProcessor; + field protected final List! logs; + field protected long started; + } + + public class ModuleCrashes.Crashes { + ctor public ModuleCrashes.Crashes(); + method public void addCrashBreadcrumb(String!); + method public void recordHandledException(Throwable!); + method public void recordHandledException(Throwable!, Map!); + method public void recordUnhandledException(Throwable!); + method public void recordUnhandledException(Throwable!, Map!); + } + + public class ModuleDeviceIdCore extends ly.count.sdk.java.internal.ModuleBase { + ctor public ModuleDeviceIdCore(); + method protected ly.count.sdk.java.Config.DID! acquireId(); + method protected void changeDeviceIdInternal(String!, ly.count.sdk.java.internal.DeviceIdType!, boolean); + method public void deviceIdChanged(String!, boolean); + method protected String! getIDInternal(); + method protected ly.count.sdk.java.internal.DeviceIdType! getTypeInternal(); + method public void init(ly.count.sdk.java.internal.InternalConfig!); + method public void initFinished(ly.count.sdk.java.internal.InternalConfig!); + method public void login(String!); + method public void logout(); + field protected ly.count.sdk.java.internal.ModuleDeviceIdCore.DeviceId! deviceIdInterface; + } + + public class ModuleDeviceIdCore.DeviceId { + ctor public ModuleDeviceIdCore.DeviceId(); + method public void changeWithMerge(String!); + method public void changeWithoutMerge(String!); + method public String! getID(); + method public ly.count.sdk.java.internal.DeviceIdType! getType(); + method public void setID(String!); + } + + public class ModuleEvents extends ly.count.sdk.java.internal.ModuleBase { + ctor public ModuleEvents(); + method public void deviceIdChanged(String!, boolean); + method protected void recordEventInternal(String!, int, Double!, Double!, Map!, String!); + field protected ly.count.sdk.java.internal.EventQueue! eventQueue; + field protected ly.count.sdk.java.internal.ModuleEvents.Events! eventsInterface; + } + + public class ModuleEvents.Events { + ctor public ModuleEvents.Events(); + method public boolean cancelEvent(String!); + method public boolean endEvent(String!); + method public boolean endEvent(String!, Map!, int, Double!); + method public void recordEvent(String!); + method public void recordEvent(String!, int); + method public void recordEvent(String!, int, Double!); + method public void recordEvent(String!, Map!); + method public void recordEvent(String!, Map!, int); + method public void recordEvent(String!, Map!, int, Double!); + method public void recordEvent(String!, Map!, int, Double!, Double!); + method public boolean startEvent(String!); + } + + public class ModuleFeedback extends ly.count.sdk.java.internal.ModuleBase { + } + + public class ModuleFeedback.Feedback { + ctor public ModuleFeedback.Feedback(); + method public String! constructFeedbackWidgetUrl(ly.count.sdk.java.internal.CountlyFeedbackWidget?); + method public String! constructFeedbackWidgetUrl(ly.count.sdk.java.internal.CountlyFeedbackWidget?, int, int); + method public void getAvailableFeedbackWidgets(ly.count.sdk.java.internal.CallbackOnFinish!>?); + method public void getFeedbackWidgetData(ly.count.sdk.java.internal.CountlyFeedbackWidget?, ly.count.sdk.java.internal.CallbackOnFinish?); + method public void reportFeedbackWidgetManually(ly.count.sdk.java.internal.CountlyFeedbackWidget?, org.json.JSONObject?, Map?); + } + + public class ModuleHealthCheck extends ly.count.sdk.java.internal.ModuleBase { + ctor public ModuleHealthCheck(); + } + + public class ModuleLocation extends ly.count.sdk.java.internal.ModuleBase { + method public void initFinished(ly.count.sdk.java.internal.InternalConfig); + method protected void saveLocationToParamsLegacy(ly.count.sdk.java.internal.Params!); + method protected void setLocationLegacy(Object?, Object?, Object?); + } + + public class ModuleLocation.Location { + ctor public ModuleLocation.Location(); + method public void disableLocation(); + method public void setLocation(String?, String?, String?, String?); + } + + public class ModuleRemoteConfig extends ly.count.sdk.java.internal.ModuleBase { + method public void initFinished(ly.count.sdk.java.internal.InternalConfig); + } + + public class ModuleRemoteConfig.RemoteConfig { + ctor public ModuleRemoteConfig.RemoteConfig(); + method public void clearAll(); + method public void downloadAllKeys(ly.count.sdk.java.internal.RCDownloadCallback?); + method public void downloadOmittingKeys(String![]?, ly.count.sdk.java.internal.RCDownloadCallback?); + method public void downloadSpecificKeys(String![]?, ly.count.sdk.java.internal.RCDownloadCallback?); + method public void enrollIntoABTestsForKeys(String![]?); + method public void exitABTestsForKeys(String![]?); + method public Map getAllValuesAndEnroll(); + method public ly.count.sdk.java.internal.RCData getValue(String?); + method public ly.count.sdk.java.internal.RCData getValueAndEnroll(String?); + method public Map getValues(); + method public void registerDownloadCallback(ly.count.sdk.java.internal.RCDownloadCallback?); + method public void removeDownloadCallback(ly.count.sdk.java.internal.RCDownloadCallback?); + } + + public class ModuleRequests extends ly.count.sdk.java.internal.ModuleBase { + ctor public ModuleRequests(); + method public static Future! changeId(ly.count.sdk.java.internal.InternalConfig!, String!); + method public void initFinished(ly.count.sdk.java.internal.InternalConfig!); + method public static void injectParams(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.ModuleRequests.ParamsInjector!); + method public static Future! location(ly.count.sdk.java.internal.InternalConfig!, double, double); + method public static ly.count.sdk.java.internal.Request! nonSessionRequest(ly.count.sdk.java.internal.InternalConfig!); + method public static ly.count.sdk.java.internal.Params! prepareRequiredParams(ly.count.sdk.java.internal.InternalConfig!); + method public static String! prepareRequiredParamsAsString(ly.count.sdk.java.internal.InternalConfig!, Object!...!); + method public static Future! pushAsync(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.Request!); + method public static Future! pushAsync(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.Request!, boolean, ly.count.sdk.java.internal.Tasks.Callback!); + method public static Future! sessionBegin(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.SessionImpl!); + method public static Future! sessionEnd(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.SessionImpl!, Long!, String!, ly.count.sdk.java.internal.Tasks.Callback!); + method public static Future! sessionUpdate(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.SessionImpl!, Long!); + } + + public static interface ModuleRequests.ParamsInjector { + method public void call(ly.count.sdk.java.internal.Params!); + } + + public class ModuleSessions extends ly.count.sdk.java.internal.ModuleBase { + ctor public ModuleSessions(); + method public void deviceIdChanged(String!, boolean); + method public void forgetSession(); + method public ly.count.sdk.java.internal.SessionImpl! getSession(); + method public void initFinished(ly.count.sdk.java.internal.InternalConfig!); + method public ly.count.sdk.java.internal.SessionImpl! session(ly.count.sdk.java.internal.InternalConfig!, Long!); + method public ly.count.sdk.java.internal.TimedEvents! timedEvents(); + } + + public class ModuleUserProfile extends ly.count.sdk.java.internal.ModuleBase { + method protected void clearInternal(); + method public void deviceIdChanged(String!, boolean); + method public void initFinished(ly.count.sdk.java.internal.InternalConfig!); + method protected void saveInternal(); + method protected void setPropertiesInternal(Map); + } + + public class ModuleUserProfile.UserProfile { + ctor public ModuleUserProfile.UserProfile(); + method public void clear(); + method public void increment(String!); + method public void incrementBy(String!, int); + method public void multiply(String!, double); + method public void pull(String!, Object!); + method public void push(String!, Object!); + method public void pushUnique(String!, Object!); + method public void save(); + method public void saveMax(String!, double); + method public void saveMin(String!, double); + method public void setOnce(String!, Object!); + method public void setProperties(Map!); + method public void setProperty(String!, Object!); + } + + public class ModuleViews extends ly.count.sdk.java.internal.ModuleBase implements ly.count.sdk.java.internal.ViewIdProvider { + method public void deviceIdChanged(String!, boolean); + method public String getCurrentViewId(); + method public String getPreviousViewId(); + method protected void setFirstViewInternal(boolean); + method public void updateGlobalViewSegmentationInternal(Map); + } + + public class ModuleViews.Views { + ctor public ModuleViews.Views(); + method public void addSegmentationToViewWithID(String, Map?); + method public void addSegmentationToViewWithName(String, Map?); + method public void pauseViewWithID(String); + method public void resumeViewWithID(String); + method public void setGlobalViewSegmentation(Map?); + method public String! startAutoStoppedView(String); + method public String! startAutoStoppedView(String, Map?); + method public String? startView(String); + method public String? startView(String, Map?); + method public void stopAllViews(Map?); + method public void stopViewWithID(String); + method public void stopViewWithID(String, Map?); + method public void stopViewWithName(String); + method public void stopViewWithName(String, Map?); + method public void updateGlobalViewSegmentation(Map?); + } + + public interface Networking { + method public boolean check(ly.count.sdk.java.internal.InternalConfig!); + method public ly.count.sdk.java.internal.Transport! getTransport(); + method public void init(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.IStorageForRequestQueue!); + method public boolean isSending(); + method public void stop(ly.count.sdk.java.internal.InternalConfig!); + } + + public class Params { + ctor public Params(); + ctor public Params(Object!...!); + ctor public Params(String!); + method public ly.count.sdk.java.internal.Params! add(ly.count.sdk.java.internal.Params!); + method public ly.count.sdk.java.internal.Params! add(Object!...!); + method public ly.count.sdk.java.internal.Params! add(String!); + method public ly.count.sdk.java.internal.Params! add(String!, Object!); + method public ly.count.sdk.java.internal.Params.Arr! arr(String!); + method public void clear(); + method public boolean equals(Object?); + method public String! get(String!); + method public boolean has(String!); + method public int hashCode(); + method public int length(); + method public Map! map(); + method public ly.count.sdk.java.internal.Params.Obj! obj(String!); + method public String! remove(String!); + method public String toString(); + } + + public static final class Params.Arr { + method public ly.count.sdk.java.internal.Params! add(); + method public ly.count.sdk.java.internal.Params.Arr! put(Collection!); + method public ly.count.sdk.java.internal.Params.Arr! put(ly.count.sdk.java.internal.JSONable!); + } + + public static final class Params.Obj { + method public ly.count.sdk.java.internal.Params! add(); + method public ly.count.sdk.java.internal.Params.Obj! put(String!, Object!); + } + + public class RCData { + ctor public RCData(Object!, boolean); + field public boolean isCurrentUsersData; + field public Object! value; + } + + public interface RCDownloadCallback { + method public void callback(ly.count.sdk.java.internal.RequestResult!, String!, boolean, Map!); + } + + public class RemoteConfigValueStore { + ctor protected RemoteConfigValueStore(org.json.JSONObject, boolean, ly.count.sdk.java.internal.Log); + method protected void cacheClearValues(); + method protected Map getAllValues(); + method protected ly.count.sdk.java.internal.RCData getValue(String); + method protected void mergeValues(Map, boolean); + field protected ly.count.sdk.java.internal.Log! L; + field protected static final int cacheValCached = 0; // 0x0 + field protected static final int cacheValFresh = 1; // 0x1 + field protected static final String keyCacheFlag = "c"; + field protected static final String keyValue = "v"; + field protected org.json.JSONObject! values; + field protected boolean valuesCanBeCached; + } + + public class Request implements ly.count.sdk.java.internal.Storable { + ctor public Request(Long!); + ctor public Request(Object!...!); + method public ly.count.sdk.java.internal.Request! endpoint(String!); + method public boolean equals(Object?); + method public static String! getStoragePrefix(); + method public int hashCode(); + method public ly.count.sdk.java.internal.Request! own(Class!); + method public Class! owner(); + method public boolean restore(byte[]!, ly.count.sdk.java.internal.Log!); + method public void setId(Long!); + method public Long! storageId(); + method public String! storagePrefix(); + method public byte[]! store(ly.count.sdk.java.internal.Log!); + method public String toString(); + field public static final String ENDPOINT = "endpoint"; + field public static final String MODULE = "module"; + field public ly.count.sdk.java.internal.Params! params; + } + + public enum RequestResult { + enum_constant public static final ly.count.sdk.java.internal.RequestResult Error; + enum_constant public static final ly.count.sdk.java.internal.RequestResult NetworkIssue; + enum_constant public static final ly.count.sdk.java.internal.RequestResult Success; + } + + public class SDKCore { + ctor public SDKCore(); + method public void attemptToSendStoredRequests(); + method protected void buildModules(ly.count.sdk.java.internal.InternalConfig!, int); + method public void changeAppKey(String?); + method @Deprecated public void changeDeviceIdWithMerge(ly.count.sdk.java.internal.InternalConfig!, String!); + method @Deprecated public void changeDeviceIdWithoutMerge(ly.count.sdk.java.internal.InternalConfig!, String!); + method public void changeServerUrl(String?); + method public ly.count.sdk.java.internal.InternalConfig! config(); + method public ly.count.sdk.java.internal.ModuleContent.Content! content(); + method public static ly.count.sdk.java.internal.ContentUrlHandler! contentUrlHandler(); + method public ly.count.sdk.java.internal.ModuleCrashes.Crashes! crashes(); + method public ly.count.sdk.java.internal.ModuleDeviceIdCore.DeviceId! deviceId(); + method public static boolean enabled(int); + method public static boolean enabled(ly.count.sdk.java.internal.CoreFeature!); + method public ly.count.sdk.java.internal.ModuleEvents.Events! events(); + method public ly.count.sdk.java.internal.ModuleFeedback.Feedback! feedback(); + method public void flushQueues(); + method public ly.count.sdk.java.internal.SessionImpl! getSession(); + method public void halt(); + method public boolean hasConsentForFeature(ly.count.sdk.java.internal.CoreFeature!); + method public void init(ly.count.sdk.java.internal.InternalConfig!); + method public Boolean! isRequestReady(ly.count.sdk.java.internal.Request!); + method public boolean isTracking(Integer!); + method public ly.count.sdk.java.internal.ModuleLocation.Location! location(); + method public static ly.count.sdk.java.internal.Log! logger(); + method public void login(String!); + method public void logout(); + method public T module(Class!); + method protected ly.count.sdk.java.internal.ModuleBase! module(ly.count.sdk.java.internal.CoreFeature!); + method public void notifyModulesDeviceIdChanged(String?, boolean); + method public void onConsent(int); + method public void onConsentRemoval(ly.count.sdk.java.internal.InternalConfig!, int); + method public void onRequest(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.Request!); + method public void onRequestCompleted(ly.count.sdk.java.internal.Request!, String!, int, Class!); + method public ly.count.sdk.java.internal.SessionImpl! onSessionBegan(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.SessionImpl!); + method public ly.count.sdk.java.internal.SessionImpl! onSessionEnded(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.SessionImpl!); + method public void onSignal(ly.count.sdk.java.internal.InternalConfig!, int); + method public void onSignal(ly.count.sdk.java.internal.InternalConfig!, int, String!); + method protected void prepareMappings(); + method protected void recover(ly.count.sdk.java.internal.InternalConfig!); + method protected static void registerDefaultModuleMappings(); + method public ly.count.sdk.java.internal.ModuleRemoteConfig.RemoteConfig! remoteConfig(); + method public void removeDifferentAppKeysFromQueue(); + method public void replaceAllAppKeysInQueueWithCurrentAppKey(); + method public static File! sdkStorageDirectory(); + method public ly.count.sdk.java.internal.SessionImpl! session(Long!); + method protected void setDeviceIdFromStorageIfExist(ly.count.sdk.java.internal.InternalConfig!); + method @Deprecated public void stop(boolean); + method public ly.count.sdk.java.internal.UserImpl! user(); + method public ly.count.sdk.java.internal.ModuleUserProfile.UserProfile! userProfile(); + method public ly.count.sdk.java.internal.ModuleViews.Views! views(); + field protected ly.count.sdk.java.internal.Log! L; + field public ly.count.sdk.java.internal.InternalConfig! config; + field protected volatile int consents; + field public static final ly.count.sdk.java.internal.ModuleContent.Content! dormantContent; + field public static final ly.count.sdk.java.internal.ModuleCrashes.Crashes! dormantCrashes; + field public static final ly.count.sdk.java.internal.ModuleDeviceIdCore.DeviceId! dormantDeviceId; + field public static final ly.count.sdk.java.internal.ModuleEvents.Events! dormantEvents; + field public static final ly.count.sdk.java.internal.ModuleFeedback.Feedback! dormantFeedback; + field public static final ly.count.sdk.java.internal.ModuleLocation.Location! dormantLocation; + field public static final ly.count.sdk.java.internal.ModuleRemoteConfig.RemoteConfig! dormantRemoteConfig; + field public static final ly.count.sdk.java.internal.ModuleUserProfile.UserProfile! dormantUserProfile; + field public static final ly.count.sdk.java.internal.ModuleViews.Views! dormantViews; + field protected static ly.count.sdk.java.internal.SDKCore! instance; + field protected final Object! lockBRQStorage; + field protected final Map! modules; + field protected ly.count.sdk.java.internal.Networking! networking; + field protected Queue! requestQueueMemory; + field protected final ly.count.sdk.java.internal.SDKStorage! sdkStorage; + field protected static ly.count.sdk.java.internal.ModuleBase! testDummyModule; + } + + public enum SDKCore.Signal { + method public int getIndex(); + enum_constant public static final ly.count.sdk.java.internal.SDKCore.Signal Crash; + enum_constant public static final ly.count.sdk.java.internal.SDKCore.Signal DID; + enum_constant public static final ly.count.sdk.java.internal.SDKCore.Signal Ping; + enum_constant public static final ly.count.sdk.java.internal.SDKCore.Signal Start; + } + + public class SDKStorage implements ly.count.sdk.java.internal.StorageProvider { + ctor protected SDKStorage(); + method public String! getDeviceID(); + method public String! getDeviceIdType(); + method public String! getHealthCheckState(); + method public Integer! getMigrationVersion(); + method public org.json.JSONObject! getRemoteConfigValues(); + method public String! getServerConfig(); + method public void incrementAutomaticCrashCount(); + method public ly.count.sdk.java.internal.SDKStorage! init(ly.count.sdk.java.internal.InternalConfig!); + method public boolean isCountlyStorageEmpty(); + method protected String! readEventQueue(); + method public void setDeviceID(String!); + method public void setDeviceIdType(String!); + method public void setHealthCheckState(String!); + method public void setMigrationVersion(Integer!); + method public void setRemoteConfigValues(org.json.JSONObject!); + method public void setServerConfig(String!); + method public void stop(ly.count.sdk.java.internal.InternalConfig!, boolean); + method public List! storableList(ly.count.sdk.java.internal.InternalConfig!, String!, int); + method public Boolean! storablePop(ly.count.sdk.java.internal.InternalConfig!, T); + method public int storablePurge(ly.count.sdk.java.internal.InternalConfig!, String!); + method public Boolean! storableRead(ly.count.sdk.java.internal.InternalConfig!, T); + method public byte[]! storableReadBytes(ly.count.sdk.java.internal.InternalConfig!, String!); + method public byte[]! storableReadBytes(ly.count.sdk.java.internal.InternalConfig!, String!, Long!); + method public Map.Entry! storableReadBytesOneOf(ly.count.sdk.java.internal.InternalConfig!, T, boolean); + method public Boolean! storableRemove(ly.count.sdk.java.internal.InternalConfig!, T); + method public Boolean! storableWrite(ly.count.sdk.java.internal.InternalConfig!, String!, Long!, byte[]!); + method public Boolean! storableWrite(ly.count.sdk.java.internal.InternalConfig!, T); + method protected void storeEventQueue(String!); + method public int takeAutomaticCrashCount(); + field protected static final String EVENT_QUEUE_FILE_NAME = "event_queue"; + field protected static final String FILE_NAME_PREFIX = "[CLY]"; + field protected static final String FILE_NAME_SEPARATOR = "_"; + field protected static final String JSON_FILE_NAME = "countly_store.json"; + field protected static final String key_device_id = "did"; + field protected static final String key_device_id_type = "did_t"; + field protected static final String key_health_check = "HEALTH_CHECK"; + field protected static final String key_health_check_automatic_crashes = "HEALTH_CHECK_AUTOMATIC_CRASHES"; + field protected static final String key_migration_version = "dv"; + field protected static final String key_remote_config = "rc"; + field protected static final String key_server_config = "sc"; + } + + public class SessionImpl implements ly.count.sdk.java.Session ly.count.sdk.java.internal.Storable { + ctor protected SessionImpl(ly.count.sdk.java.internal.InternalConfig!); + ctor public SessionImpl(ly.count.sdk.java.internal.InternalConfig!, Long!); + method public ly.count.sdk.java.Session! addCrashReport(Throwable!, boolean); + method public ly.count.sdk.java.Session! addCrashReport(Throwable!, boolean, String!, Map!, String!...!); + method public ly.count.sdk.java.Session! addLocation(double, double); + method public ly.count.sdk.java.Session! addParam(String!, Object!); + method public ly.count.sdk.java.Session! begin(); + method @Deprecated public ly.count.sdk.java.Usage! changeDeviceIdWithMerge(String!); + method @Deprecated public ly.count.sdk.java.Usage! changeDeviceIdWithoutMerge(String!); + method public void end(); + method public boolean equals(Object?); + method public ly.count.sdk.java.Event! event(String!); + method public Long! getBegan(); + method @Deprecated public String! getDeviceId(); + method public Long! getEnded(); + method public Long! getId(); + method public static String! getStoragePrefix(); + method public int hashCode(); + method public boolean isActive(); + method public ly.count.sdk.java.Usage! login(String!); + method public ly.count.sdk.java.Usage! logout(); + method @Deprecated public void recordEvent(ly.count.sdk.java.Event!); + method public boolean restore(byte[]!, ly.count.sdk.java.internal.Log!); + method public void setId(Long!); + method public Long! storageId(); + method public String! storagePrefix(); + method public byte[]! store(ly.count.sdk.java.internal.Log!); + method @Deprecated public ly.count.sdk.java.Event! timedEvent(String!); + method @Deprecated protected ly.count.sdk.java.internal.TimedEvents! timedEvents(); + method public String toString(); + method public ly.count.sdk.java.Session! update(); + method public ly.count.sdk.java.User! user(); + method @Deprecated public ly.count.sdk.java.View! view(String!); + method @Deprecated public ly.count.sdk.java.View! view(String!, boolean); + field protected ly.count.sdk.java.internal.Log! L; + field protected Long! began; + field protected ly.count.sdk.java.View! currentView; + field protected Long! ended; + field protected final List! events; + field protected Long! id; + field protected final ly.count.sdk.java.internal.Params! params; + field protected boolean startView; + field protected Long! updated; + } + + public interface Storable extends ly.count.sdk.java.internal.Byteable { + method public void setId(Long!); + method public Long! storageId(); + method public String! storagePrefix(); + } + + public class Storage { + ctor public Storage(); + method public static void await(ly.count.sdk.java.internal.Log!); + method public static List! list(ly.count.sdk.java.internal.InternalConfig!, String!); + method public static List! list(ly.count.sdk.java.internal.InternalConfig!, String!, int); + method public static Future!>! listAsync(ly.count.sdk.java.internal.InternalConfig!, String!, int); + method public static String! name(ly.count.sdk.java.internal.Storable!); + method public static T pop(ly.count.sdk.java.internal.InternalConfig!, T); + method public static Future! popAsync(ly.count.sdk.java.internal.InternalConfig!, T); + method public static boolean push(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.Storable!); + method public static Future! pushAsync(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.Storable!); + method public static Future! pushAsync(ly.count.sdk.java.internal.InternalConfig!, ly.count.sdk.java.internal.Storable!, ly.count.sdk.java.internal.Tasks.Callback!); + method public static T read(ly.count.sdk.java.internal.InternalConfig!, T); + method public static Future! readAsync(ly.count.sdk.java.internal.InternalConfig!, T); + method public static Future! readAsync(ly.count.sdk.java.internal.InternalConfig!, T, ly.count.sdk.java.internal.Tasks.Callback!); + method public static T readOne(ly.count.sdk.java.internal.InternalConfig!, T, boolean); + method public static Future! readOneAsync(ly.count.sdk.java.internal.InternalConfig!, T, boolean); + method public static Boolean! remove(ly.count.sdk.java.internal.InternalConfig!, T); + method public static Future! removeAsync(ly.count.sdk.java.internal.InternalConfig!, T, ly.count.sdk.java.internal.Tasks.Callback!); + method public static void stop(); + } + + public interface StorageProvider { + method public String! getDeviceID(); + method public String! getDeviceIdType(); + method public String! getHealthCheckState(); + method public Integer! getMigrationVersion(); + method public org.json.JSONObject! getRemoteConfigValues(); + method public String! getServerConfig(); + method public void incrementAutomaticCrashCount(); + method public boolean isCountlyStorageEmpty(); + method public void setDeviceID(String!); + method public void setDeviceIdType(String!); + method public void setHealthCheckState(String!); + method public void setMigrationVersion(Integer!); + method public void setRemoteConfigValues(org.json.JSONObject!); + method public void setServerConfig(String!); + method public int takeAutomaticCrashCount(); + } + + public class Tasks { + ctor public Tasks(String!, ly.count.sdk.java.internal.Log!); + method public Future! run(ly.count.sdk.java.internal.Tasks.Task!); + field public static final Long ID_LIST; + field public static final Long ID_STRICT; + } + + public static interface Tasks.Callback { + method public void call(T); + } + + public abstract static class Tasks.Task { + method public abstract T call(); + } + + public class TimeUtils { + ctor public TimeUtils(); + method public static int currentDayOfWeek(); + method public static int currentHour(); + method public static ly.count.sdk.java.internal.TimeUtils.Instant! getCurrentInstant(); + method protected static ly.count.sdk.java.internal.TimeUtils.Instant! getCurrentInstant(long); + method public static ly.count.sdk.java.internal.TimeUtils.Instant! getCurrentInstantUnique(); + method public static long nsToMs(long); + method public static long nsToSec(long); + method public static long secToMs(long); + method public static long secToNs(long); + method public static long timestampMs(); + method public static long uniqueStorageId(); + method public static long uniqueTimestampMs(); + method public static long uniqueTimestampS(); + field protected static final long MS_IN_SECOND = 1000L; // 0x3e8L + field protected static final Double NS_IN_MS; + field protected static final Double NS_IN_SECOND; + } + + public static class TimeUtils.Instant { + field public final int dow; + field public final int hour; + field public final long timestamp; + field public final int tz; + } + + public interface Transformer { + method public byte[]! doTheJob(Long!, byte[]!); + } + + public class Transport { + ctor public Transport(); + method public void checkClientTrusted(X509Certificate![]!, String!); + method public void checkServerTrusted(X509Certificate![]!, String!); + method public X509Certificate![]! getAcceptedIssuers(); + method public void init(ly.count.sdk.java.internal.InternalConfig!); + method public HttpURLConnection! openConnection(String!, String!, boolean); + method public ly.count.sdk.java.internal.Tasks.Task! send(ly.count.sdk.java.internal.Request!); + } + + public class UserEditorImpl implements ly.count.sdk.java.UserEditor { + method public ly.count.sdk.java.UserEditor! addToCohort(String!); + method public ly.count.sdk.java.User! commit(); + method public ly.count.sdk.java.UserEditor! inc(String!, int); + method public ly.count.sdk.java.UserEditor! max(String!, double); + method public ly.count.sdk.java.UserEditor! min(String!, double); + method public ly.count.sdk.java.UserEditor! mul(String!, double); + method public ly.count.sdk.java.UserEditor! optOutFromLocationServices(); + method public ly.count.sdk.java.UserEditor! pull(String!, Object!); + method public ly.count.sdk.java.UserEditor! push(String!, Object!); + method public ly.count.sdk.java.UserEditor! pushUnique(String!, Object!); + method public ly.count.sdk.java.UserEditor! removeFromCohort(String!); + method public ly.count.sdk.java.UserEditor! set(String!, Object!); + method public ly.count.sdk.java.UserEditor! setBirthyear(int); + method public ly.count.sdk.java.UserEditor! setBirthyear(String!); + method public ly.count.sdk.java.UserEditor! setCity(String!); + method public ly.count.sdk.java.UserEditor! setCountry(String!); + method public ly.count.sdk.java.UserEditor! setCustom(String!, Object!); + method public ly.count.sdk.java.UserEditor! setEmail(String!); + method public ly.count.sdk.java.UserEditor! setGender(Object!); + method public ly.count.sdk.java.UserEditor! setLocale(String!); + method public ly.count.sdk.java.UserEditor! setLocation(double, double); + method public ly.count.sdk.java.UserEditor! setLocation(String!); + method public ly.count.sdk.java.UserEditor! setName(String!); + method public ly.count.sdk.java.UserEditor! setOnce(String!, Object!); + method public ly.count.sdk.java.UserEditor! setOrg(String!); + method public ly.count.sdk.java.UserEditor! setPhone(String!); + method public ly.count.sdk.java.UserEditor! setPicture(byte[]!); + method public ly.count.sdk.java.UserEditor! setPicturePath(String!); + method public ly.count.sdk.java.UserEditor! setUsername(String!); + } + + public class UserImpl extends ly.count.sdk.java.User { + ctor public UserImpl(ly.count.sdk.java.internal.InternalConfig!); + method public Integer! birthyear(); + method public String! city(); + method public String! country(); + method public Map! custom(); + method public ly.count.sdk.java.UserEditor! edit(); + method public String! email(); + method public ly.count.sdk.java.User.Gender! gender(); + method public String! id(); + method public String! locale(); + method public String! location(); + method public String! name(); + method public String! org(); + method public String! phone(); + method public byte[]! picture(); + method public String! picturePath(); + method public String toString(); + method public String! username(); + } + + public class Utils { + ctor public Utils(); + method public static boolean contains(String!, String!); + method public static String! digestHex(String!, String!, ly.count.sdk.java.internal.Log!); + method public static Map! fixSegmentKeysAndValues(int, int, Map!, ly.count.sdk.java.internal.Log!); + method public static String! hex(byte[]!); + method public static boolean isEmptyOrNull(String!); + method public static boolean isEqual(Object!, Object!); + method public static boolean isNotEmpty(String!); + method public static boolean isNotEqual(Object!, Object!); + method public static boolean isValidDataType(Object!); + method public static boolean isValidURL(String!); + method public static String! join(Collection!, String!); + method public static String! readFileContent(File!, ly.count.sdk.java.internal.Log!); + method public static byte[]! readStream(InputStream!, ly.count.sdk.java.internal.Log!); + method public static List! reflectiveGetDeclaredFields(Class!, boolean); + method public static List! reflectiveGetDeclaredFields(List!, Class!, boolean); + method public static void removeInvalidDataFromSegments(Map!, ly.count.sdk.java.internal.Log!); + method public static String! safeRandomVal(); + method public static String! trimKey(int, String!, ly.count.sdk.java.internal.Log!); + method public static String! trimValue(int, String!, String!, ly.count.sdk.java.internal.Log!); + method public static String![]! trimValues(int, String![]!, ly.count.sdk.java.internal.Log!); + method public static String! urldecode(String!); + method public static String! urlencode(String!, ly.count.sdk.java.internal.Log!); + method protected static String! urlencode(String!, ly.count.sdk.java.internal.Log!, String!); + field public static final char[]! BASE_16; + field public static final String CRLF = "\r\n"; + field public static final String UTF8 = "UTF-8"; + } + + public static class Utils.Base64 { + ctor public Utils.Base64(); + method public static byte[]! decode(String!, ly.count.sdk.java.internal.Log!); + method public static String! decodeToString(String!, ly.count.sdk.java.internal.Log!); + method public static String! encode(byte[]!); + method public static String! encode(String!); + } + + public interface ViewIdProvider { + method public String getCurrentViewId(); + method public String getPreviousViewId(); + } + + public class WidgetAction { + ctor public WidgetAction(); + method public ly.count.sdk.java.internal.ContentPlacement! resizeFor(boolean); + field public boolean close; + field public String! eventPayload; + field public boolean hasResize; + field public boolean isActionEvent; + field public boolean isExternalLink; + field public boolean isSdkSignal; + field public boolean isWidgetCommand; + field public ly.count.sdk.java.internal.ContentPlacement! landscape; + field public String! link; + field public ly.count.sdk.java.internal.ContentPlacement! portrait; + field public Map! queryParams; + } + + public class WidgetActionParser { + method public static ly.count.sdk.java.internal.WidgetAction! parse(String!, ly.count.sdk.java.internal.Log!); + field public static final String ACTION_HOST = "countly_action_event"; + field public static final String! ACTION_URL_START = "https://countly_action_event"; + } + +} + diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/RequestScenarioTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/RequestScenarioTests.java new file mode 100644 index 00000000..70b2fb73 --- /dev/null +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/RequestScenarioTests.java @@ -0,0 +1,541 @@ +package ly.count.sdk.java.internal; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import ly.count.sdk.java.Config; +import ly.count.sdk.java.Countly; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * In each request scenario the SDK queues the requests sdk-java/api/requests/[scenario].json records, with the values + * that change from run to run masked. The scenarios make the calls the other Countly SDKs make in their own request + * tests. A deliberate change: run COUNTLY_UPDATE_REQUESTS=1 ./gradlew :sdk-java:test --tests '*RequestScenarioTests' + * --rerun, which writes the records, check the difference and add a line to CHANGELOG.md. + */ +@RunWith(JUnit4.class) +public class RequestScenarioTests { + + private static final String MASK = ""; + private static final List MASKED_PARAMS = Arrays.asList("timestamp", "hour", "dow", "tz", "sdk_version", "av", "rr", "checksum", "checksum256", "session_duration", "session_id"); + private static final List JSON_PARAMS = Arrays.asList("events", "metrics", "user_details", "crash", "consent", "hc"); + private static final List MASKED_EVENT_FIELDS = Arrays.asList("timestamp", "hour", "dow", "id", "pvid", "cvid", "peid"); + private static final List MASKED_VIEW_FIELDS = Arrays.asList("timestamp", "hour", "dow", "id", "pvid", "cvid", "peid", "dur"); + private static final List MASKED_CRASH_FIELDS = Arrays.asList("_error", "_run", "_os", "_os_version", "_resolution", "_density", "_locale", "_device", "_device_type", "_manufacture", "_cpu", "_opengl", "_ram_current", "_ram_total", "_disk_current", "_disk_total", "_bat", "_orientation", "_root", "_online", "_muted", "_background"); + // a view's "segment" is the operating system the SDK runs on + private static final List MASKED_VIEW_SEGMENTS = Arrays.asList("segment"); + private static final List FULLY_MASKED_PARAMS = Arrays.asList("metrics", "hc"); + private static final String EVENTS_PARAM = "events"; + private static final String CRASH_PARAM = "crash"; + private static final String VIEW_KEY = "[CLY]_view"; + private static final File RECORDS = new File("api/requests"); + private static final long SETTLE_QUIET_MS = 500; + private static final long SETTLE_LIMIT_MS = 10_000; + private static final char FIRST_PRINTABLE = ' '; + + /** + * The calls of a scenario. + */ + private interface Scenario { + /** + * Makes the calls of the scenario. + * + * @throws InterruptedException when waiting for the SDK is interrupted + */ + void run() throws InterruptedException; + } + + /** + * Starts every scenario from an empty SDK storage, with fixed device details, no metric overrides that other tests + * left and without the timer of a test that shortened it. + * + * @throws ReflectiveOperationException when the device's metric overrides cannot be reached + */ + @Before + @SuppressWarnings("PMD.AvoidAccessibilityAlteration") + public void setUp() throws ReflectiveOperationException { + TestUtils.createCleanTestState(); + CountlyTimer.TIMER_DELAY_MS = 0; + TestUtils.setAdditionalDeviceMetrics(); + // every test shares the device, and its API can only add metric overrides + Field metricOverride = Device.class.getDeclaredField("metricOverride"); + metricOverride.setAccessible(true); + ((Map) metricOverride.get(Device.dev)).clear(); + } + + /** + * Stops the SDK and empties its storage. + */ + @After + public void tearDown() { + TestUtils.createCleanTestState(); + } + + /** + * A session with an event of every segmentation value type, the user details and a session update. + * + * @throws IOException when the record cannot be read or written + * @throws InterruptedException when waiting for the SDK is interrupted + */ + @Test + public void parity() throws IOException, InterruptedException { + assertRequests("parity", config(), () -> { + Countly.session().begin(); + Countly.instance().events().recordEvent("purchase", paritySegmentation(), 2, 9.99, 5.0); + Map properties = new ConcurrentHashMap<>(); + properties.put("name", "John Doe"); + properties.put("username", "john_d"); + properties.put("email", "john@example.com"); + properties.put("organization", "Countly"); + properties.put("phone", "1234567890"); + properties.put("picture", "https://example.com/picture.jpg"); + properties.put("gender", "M"); + properties.put("byear", 1980); + properties.put("tier", "gold"); + properties.put("age", 30); + Countly.instance().userProfile().setProperties(properties); + Countly.instance().userProfile().save(); + // a session update reports elapsed seconds, so at least one has to pass for it to be sent + Thread.sleep(1100); + Countly.session().update(); + Countly.session().end(); + }); + } + + /** + * Two views in a session, the first with segmentation. + * + * @throws IOException when the record cannot be read or written + * @throws InterruptedException when waiting for the SDK is interrupted + */ + @Test + public void views() throws IOException, InterruptedException { + assertRequests("views", config(), () -> { + Countly.session().begin(); + Map segmentation = new ConcurrentHashMap<>(); + segmentation.put("source", "scenario"); + Countly.instance().views().startView("Home", segmentation); + Countly.instance().views().stopViewWithName("Home"); + Countly.instance().views().startView("Settings"); + Countly.session().end(); + }); + } + + /** + * A handled exception with a breadcrumb and segmentation. + * + * @throws IOException when the record cannot be read or written + * @throws InterruptedException when waiting for the SDK is interrupted + */ + @Test + public void crash() throws IOException, InterruptedException { + assertRequests("crash", config(), () -> { + Countly.instance().crashes().addCrashBreadcrumb("breadcrumb one"); + Map segmentation = new ConcurrentHashMap<>(); + segmentation.put("tier", "gold"); + Countly.instance().crashes().recordHandledException(new Exception("parity crash"), segmentation); + }); + } + + /** + * Consent given for sessions and events, then taken back for events. + * + * @throws IOException when the record cannot be read or written + * @throws InterruptedException when waiting for the SDK is interrupted + */ + @Test + public void consent() throws IOException, InterruptedException { + assertRequests("consent", config().setRequiresConsent(true), () -> { + Countly.onConsent(Config.Feature.Sessions, Config.Feature.Events); + Countly.session().begin(); + Countly.instance().events().recordEvent("consented"); + settle(); + Countly.onConsentRemoval(Config.Feature.Events); + Countly.instance().events().recordEvent("not_consented"); + Countly.session().end(); + }); + } + + /** + * A device ID change with merge, then one without. + * + * @throws IOException when the record cannot be read or written + * @throws InterruptedException when waiting for the SDK is interrupted + */ + @Test + public void deviceId() throws IOException, InterruptedException { + assertRequests("device-id", config(), () -> { + Countly.session().begin(); + Countly.instance().deviceId().changeWithMerge("parity-device-002"); + Countly.instance().events().recordEvent("after_merge"); + settle(); + Countly.instance().deviceId().changeWithoutMerge("parity-device-003"); + Countly.instance().events().recordEvent("after_new_id"); + Countly.session().end(); + }); + } + + /** + * The init config every scenario starts from: the parity app key and device ID, a server that cannot be reached so + * that every request stays queued, and the features the scenarios use. + * + * @return config + */ + private Config config() { + return new Config("https://parity.example", "parity-app-key", TestUtils.getTestSDirectory()) + .setCustomDeviceId("parity-device-001") + .enableFeatures(Config.Feature.Events, Config.Feature.Sessions, Config.Feature.Views, Config.Feature.CrashReporting, Config.Feature.UserProfiles) + .setLoggingLevel(Config.LoggingLevel.VERBOSE); + } + + /** + * The event segmentation of the parity scenario, with one value of every type. + * + * @return segmentation + */ + private Map paritySegmentation() { + Map segmentation = new ConcurrentHashMap<>(); + segmentation.put("nar", "uto"); + segmentation.put("sasuke", Integer.MAX_VALUE); + segmentation.put("sakura", false); + segmentation.put("itachi", Double.MAX_VALUE); + segmentation.put("kakaski", Double.MIN_VALUE); + segmentation.put("hashirama", -Integer.MAX_VALUE); + Map tobi = new ConcurrentHashMap<>(); + tobi.put("obito", "uchiha"); + tobi.put("zetsu", "white"); + segmentation.put("tobi", tobi); + segmentation.put("stringList", Arrays.asList("value1", "value2", "value3")); + segmentation.put("intList", Arrays.asList(1, 2, 3)); + segmentation.put("doubleList", Arrays.asList(1.1, 2.2, 3.3)); + segmentation.put("boolList", Arrays.asList(true, false, true)); + segmentation.put("mixedList", Arrays.asList("value1", 2, 3.3, true)); + segmentation.put("mapList", Arrays.asList(mapListEntry(1), mapListEntry(2), mapListEntry(3))); + return segmentation; + } + + /** + * An entry of the parity scenario's list of objects. + * + * @param index position of the entry, from 1 + * @return the entry + */ + private Map mapListEntry(int index) { + Map entry = new ConcurrentHashMap<>(); + entry.put("key1", "value" + index); + entry.put("key2", index + 1); + return entry; + } + + /** + * Moves the recorded events into the request queue and waits until the queue stops growing, so that what the calls + * before queued stays apart from what follows. + * + * @throws InterruptedException when waiting is interrupted + */ + private void settle() throws InterruptedException { + // without the events consent the module is not loaded + ModuleEvents events = SDKCore.instance.module(ModuleEvents.class); + if (events != null) { + events.onTimer(); + } + int count = -1; + long quietSince = System.currentTimeMillis(); + long started = quietSince; + while (System.currentTimeMillis() - started < SETTLE_LIMIT_MS) { + Thread.sleep(50); + int queued = TestUtils.getCurrentRQ().length; + if (queued == count) { + if (System.currentTimeMillis() - quietSince > SETTLE_QUIET_MS) { + return; + } + } else { + count = queued; + quietSince = System.currentTimeMillis(); + } + } + Assert.fail("the request queue kept changing for 10 seconds"); + } + + /** + * Runs a scenario on a new SDK instance and compares the requests it queued with the record of the scenario, or + * writes the record when COUNTLY_UPDATE_REQUESTS is set. + * + * @param name scenario name, the record file name without .json + * @param config init config + * @param scenario the calls + * @throws IOException when the record cannot be read or written + * @throws InterruptedException when waiting for the SDK is interrupted + */ + private void assertRequests(String name, Config config, Scenario scenario) throws IOException, InterruptedException { + Countly.instance().init(config); + scenario.run(); + settle(); + String current = report(TestUtils.getCurrentRQ()); + File file = new File(RECORDS, name + ".json"); + if (System.getenv("COUNTLY_UPDATE_REQUESTS") != null) { + Files.createDirectories(RECORDS.toPath()); + Files.write(file.toPath(), current.getBytes(StandardCharsets.UTF_8)); + return; + } + String committed = file.exists() ? new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8).replace("\r\n", "\n") : "(no sdk-java/api/requests/" + name + ".json)\n"; + Assert.assertEquals("the requests of the " + name + " scenario differ from sdk-java/api/requests/" + name + ".json. If that is deliberate, run COUNTLY_UPDATE_REQUESTS=1 ./gradlew :sdk-java:test --tests '*RequestScenarioTests' --rerun, check the difference and add a line to CHANGELOG.md.", committed, current); + } + + /** + * The record of queued requests as the SDK sends them: each with its path and its parameters, the values that + * change from run to run masked, the JSON parameters parsed and the keys of every object sorted. + * + * @param requests the request queue, as TestUtils.getCurrentRQ gives it + * @return file content + */ + private String report(Map[] requests) { + StringBuilder out = new StringBuilder(8192).append('['); + for (int i = 0; i < requests.length; i++) { + out.append(i == 0 ? "\n" : ",\n").append(" {\n \"path\": "); + string(out, pathOf(requests[i])); + out.append(",\n \"params\": "); + write(out, recordedParams(requests[i]), " "); + out.append("\n }"); + } + return out.append(requests.length == 0 ? "]\n" : "\n]\n").toString(); + } + + /** + * The path a queued request is sent to: its endpoint without the query mark, or /i. + * + * @param request the queued request's parameters + * @return the path + */ + private String pathOf(Map request) { + String endpoint = request.get(Request.ENDPOINT); + return endpoint == null ? "/i" : endpoint.replaceAll("\\?$", ""); + } + + /** + * The parameters of a queued request as the SDK sends them, with the values that change from run to run masked and + * the JSON parameters parsed. + * + * @param request the queued request's parameters + * @return the parameters + */ + private JSONObject recordedParams(Map request) { + JSONObject params = new JSONObject(); + for (Map.Entry param : request.entrySet()) { + String name = param.getKey(); + // the SDK takes both out of a queued request before it sends it + if (Request.MODULE.equals(name) || Request.ENDPOINT.equals(name)) { + continue; + } + if (MASKED_PARAMS.contains(name)) { + params.put(name, MASK); + } else if (JSON_PARAMS.contains(name) && !param.getValue().isEmpty()) { + params.put(name, maskedJson(name, parsed(param.getValue()))); + } else { + params.put(name, param.getValue()); + } + } + return params; + } + + /** + * The value a JSON parameter holds. + * + * @param json the parameter's text + * @return the parsed value + */ + private Object parsed(String json) { + return new JSONTokener(json).nextValue(); + } + + /** + * The value of a JSON parameter with the values that change from run to run masked. + * + * @param name parameter name + * @param value parsed parameter value + * @return the masked value + */ + private Object maskedJson(String name, Object value) { + if (FULLY_MASKED_PARAMS.contains(name) && value instanceof JSONObject) { + return masked((JSONObject) value, keys((JSONObject) value)); + } + if (EVENTS_PARAM.equals(name) && value instanceof JSONArray) { + JSONArray events = (JSONArray) value; + JSONArray result = new JSONArray(); + for (int i = 0; i < events.length(); i++) { + result.put(maskedEvent(events.getJSONObject(i))); + } + return result; + } + if (CRASH_PARAM.equals(name) && value instanceof JSONObject) { + return masked((JSONObject) value, MASKED_CRASH_FIELDS); + } + return value; + } + + /** + * An event with the values that change from run to run masked, for a view also in its segmentation. + * + * @param event the event + * @return the masked copy + */ + private JSONObject maskedEvent(JSONObject event) { + if (!VIEW_KEY.equals(event.optString("key"))) { + return masked(event, MASKED_EVENT_FIELDS); + } + JSONObject view = masked(event, MASKED_VIEW_FIELDS); + JSONObject segmentation = view.optJSONObject("segmentation"); + if (segmentation != null) { + view.put("segmentation", masked(segmentation, MASKED_VIEW_SEGMENTS)); + } + return view; + } + + /** + * A copy of an object with the values of the given keys masked. + * + * @param value object + * @param fields keys to mask + * @return the masked copy + */ + private JSONObject masked(JSONObject value, List fields) { + JSONObject copy = new JSONObject(); + for (String key : keys(value)) { + copy.put(key, fields.contains(key) ? MASK : value.get(key)); + } + return copy; + } + + /** + * The keys of an object, sorted. + * + * @param value object + * @return keys + */ + private List keys(JSONObject value) { + TreeSet keys = new TreeSet<>(); + Iterator it = value.keys(); + while (it.hasNext()) { + keys.add(it.next()); + } + return new ArrayList<>(keys); + } + + /** + * Writes a JSON value with two-space indentation and sorted object keys, escaping only what JSON requires. + * + * @param out where to write + * @param value JSON value + * @param indent indentation of the line the value starts on + */ + private void write(StringBuilder out, Object value, String indent) { + if (value instanceof JSONObject) { + writeObject(out, (JSONObject) value, indent); + } else if (value instanceof JSONArray) { + writeArray(out, (JSONArray) value, indent); + } else if (value instanceof String) { + string(out, (String) value); + } else if (value == null || value == JSONObject.NULL) { + out.append("null"); + } else { + out.append(value); + } + } + + /** + * Writes a JSON object with its keys sorted. + * + * @param out where to write + * @param value the object + * @param indent indentation of the line the object starts on + */ + private void writeObject(StringBuilder out, JSONObject value, String indent) { + List keys = keys(value); + if (keys.isEmpty()) { + out.append("{}"); + return; + } + out.append('{'); + for (int i = 0; i < keys.size(); i++) { + out.append(i == 0 ? "\n" : ",\n").append(indent).append(" "); + string(out, keys.get(i)); + out.append(": "); + write(out, value.get(keys.get(i)), indent + " "); + } + out.append('\n').append(indent).append('}'); + } + + /** + * Writes a JSON array. + * + * @param out where to write + * @param array the array + * @param indent indentation of the line the array starts on + */ + private void writeArray(StringBuilder out, JSONArray array, String indent) { + if (array.length() == 0) { + out.append("[]"); + return; + } + out.append('['); + for (int i = 0; i < array.length(); i++) { + out.append(i == 0 ? "\n" : ",\n").append(indent).append(" "); + write(out, array.get(i), indent + " "); + } + out.append('\n').append(indent).append(']'); + } + + /** + * Writes a JSON string, escaping quotes, backslashes and control characters. + * + * @param out where to write + * @param value text + */ + private void string(StringBuilder out, String value) { + out.append('"'); + for (char c : value.toCharArray()) { + switch (c) { + case '"': + out.append("\\\""); + break; + case '\\': + out.append("\\\\"); + break; + case '\n': + out.append("\\n"); + break; + case '\r': + out.append("\\r"); + break; + case '\t': + out.append("\\t"); + break; + default: + if (c < FIRST_PRINTABLE) { + out.append(String.format("\\u%04x", (int) c)); + } else { + out.append(c); + } + break; + } + } + out.append('"'); + } +}