Adjust SDK v5を読み解く[Android]

# この記事は何？
Adjustのコードを読んで何となく語学の為にメモしたものです。
長いので生成AIに読み込ませて要約して貰って適宜質問を投げるといった使い方が良いかと思います。

# Adjustとは
[Adjust](https://www.adjust.com/ja/)は、アプリの最適化とグロースを支援するソリューションです。
複数のプラットフォームを横断する計測技術で、アプリマーケティングの全過程においてマーケターをサポートします。

(公式サイトより引用)

実際に楽天アプリを使ってかみ砕いて説明すると、[楽天市場サービス一覧](https://www.rakuten.co.jp/sitemap/sp/app.html)にあるような
https://app.adjust.com/dj7tmxv のようなadjustリンクを踏んだ際に

1. Adjustがリンクをクリックしたことを記録
2. iOSはapp storeへ、androidはGoogle Play Storeにリダイレクトする
3. アプリを初回起動時にインストールされたことをAdjustに送信
4. 1と3を紐づけて、「どの経路でインストールしたユーザーが、どのようなアクションを実行したか」をログ分析できる

という物です。

そこまで導入のハードルは高くなく、Android開発者であれば1時間無いぐらいで実装が終わるぐらいです。

# Adjust SDKとは
Adjustを導入する為のSDKです。
v4からv5へのメジャーアップデートがあるのでついでに今回の記事を書くことにしました。

(v4の開発者へ) 具体的に何が変更になったかはこのページを見ると良いです。
[SDK v5移行ガイド iOS](https://dev.adjust.com/ja/sdk/ios/migration/v4-to-v5)
[SDK v5移行ガイド Android](https://dev.adjust.com/ja/sdk/android/migration/v4-to-v5)

今回はSDK v5のAdjuts SDK Android版を見ます
見るレポジトリ: [adjust/adjust_sdk](https://github.com/adjust/android_sdk)
見るtag: [v5.0.0](https://github.com/adjust/android_sdk/tree/v5.0.0)

# 本編
## 進め方
[連携ガイド](https://dev.adjust.com/ja/sdk/android/?version=v5)を元に、このコードを打つとどう動くんだ？を読み解いていきます。

## インストール計測
```kotlin
val appToken = "{YourAppToken}"
val environment = AdjustConfig.ENVIRONMENT_SANDBOX;
val config = AdjustConfig(this, appToken, environment)
config.setLogLevel(LogLevel.VERBOSE)
Adjust.initSdk(config)
```

`AdjustConfig.ENVIRONMENT_SANDBOX;` は開発モード/本番モードを設定するためのpublic stringです。
[AdjustConfig.java L50-L51](https://github.com/adjust/android_sdk/blob/2fa1a0a4fcaf6adbe045faa9822f0bb46bbf911e/Adjust/sdk-core/src/main/java/com/adjust/sdk/AdjustConfig.java#L50-L51)

`AdjustConfig(this, appToken, environment)` を呼び出すとコレが呼び出されます。
```java
public AdjustConfig(Context context, String appToken, String environment) {
    init(context, appToken, environment, false);
}
```

自前のinit関数が呼ばれます。第四引数がfalseで固定ですね。何でしょうコレ。見てみましょう
```java
private void init(Context context, String appToken, String environment, boolean allowSuppressLogLevel)
```

とあるように、 `allowSuppressLogLevel` が常時falseになるようです。ログ消したい人はコッチを使った方が良いですね。
```java
public AdjustConfig(Context context, String appToken, String environment, boolean allowSuppressLogLevel) {
    init(context, appToken, environment, allowSuppressLogLevel);
}
```
## AdjustConfig init
話がそれました。init関数を読み解いていきます。

```java
private void init(Context context, String appToken, String environment, boolean allowSuppressLogLevel) {
    logger = AdjustFactory.getLogger();
    // default values
    if (allowSuppressLogLevel && AdjustConfig.ENVIRONMENT_PRODUCTION.equals(environment)) {
        setLogLevel(LogLevel.SUPRESS, environment);
    } else {
        setLogLevel(LogLevel.INFO, environment);
    }

    // Always use application context
    if (context != null) {
        context = context.getApplicationContext();
    }

    this.context = context;
    this.appToken = appToken;
    this.environment = environment;

    // default values
    this.isSendingInBackgroundEnabled = false;
    this.isPreinstallTrackingEnabled = false;
    this.isDeviceIdsReadingOnceEnabled = false;
    this.coppaComplianceEnabled = false;
    this.playStoreKidsComplianceEnabled = false;
}
```

[AdjustConfig.java L62-L86](https://github.com/adjust/android_sdk/blob/2fa1a0a4fcaf6adbe045faa9822f0bb46bbf911e/Adjust/sdk-core/src/main/java/com/adjust/sdk/AdjustConfig.java#L62-L86)

複雑な事はしていませんね。

contextとappToken, environmentをそのまま渡しているだけです。

AdjustFactoryを見に行くとloggerは
```java
public static ILogger getLogger() {
    if (logger == null) {
        // Logger needs to be "static" to retain the configuration throughout the app
        logger = new Logger();
    }
    return logger;
}
```
なので特段変わった事は無いです。 `setLogLevel`も
```java
private void setLogLevel(LogLevel logLevel, String environment) {
    logger.setLogLevel(logLevel, AdjustConfig.ENVIRONMENT_PRODUCTION.equals(environment));
}
```
なので特に説明は不要かと思います。

## Adjust initSdk
`Adjust.initSdk(config)` 

でAdjustの初期化をしています。
[Adjust.java](https://github.com/adjust/android_sdk/blob/v5.0.0/Adjust/sdk-core/src/main/java/com/adjust/sdk/Adjust.java)

```java
public static void initSdk(AdjustConfig adjustConfig) {
    AdjustInstance adjustInstance = Adjust.getDefaultInstance();
    adjustInstance.initSdk(adjustConfig);
}
```

defaultInstanceを取得して、initSdkをしていますね。

```java
private static AdjustInstance defaultInstance;

public static synchronized AdjustInstance getDefaultInstance() {
@SuppressWarnings("unused")
String VERSION = "!SDK-VERSION-STRING!:com.adjust.sdk:adjust-android:5.0.0";

if (defaultInstance == null) {
    defaultInstance = new AdjustInstance();
}
return defaultInstance;
}
```
シングルトンですね。このクラスでは複雑な事は特にしておらず、 [AdjustInstance.java](https://github.com/adjust/android_sdk/blob/v5.0.0/Adjust/sdk-core/src/main/java/com/adjust/sdk/AdjustInstance.java)で処理を実行しているようです。読み解いていきましょう。

```java
public void initSdk(final AdjustConfig adjustConfig) {
    if (!AdjustSigner.isPresent()) {
        AdjustFactory.getLogger().error("Missing signature library, SDK can't be initialised");
        return;
    }
    if (adjustConfig == null) {
        AdjustFactory.getLogger().error("AdjustConfig missing");
        return;
    }
    if (!adjustConfig.isValid()) {
        AdjustFactory.getLogger().error("AdjustConfig not initialized correctly");
        return;
    }
    if (activityHandler != null) {
        AdjustFactory.getLogger().error("Adjust already initialized");
        return;
    }

    adjustConfig.preLaunchActions = preLaunchActions;
    adjustConfig.pushToken = pushToken;
    adjustConfig.startEnabled = startEnabled;
    adjustConfig.startOffline = startOffline;
    adjustConfig.basePath = this.basePath;
    adjustConfig.gdprPath = this.gdprPath;
    adjustConfig.subscriptionPath = this.subscriptionPath;
    adjustConfig.purchaseVerificationPath = this.purchaseVerificationPath;
    adjustConfig.cachedDeeplinkResolutionCallback = cachedDeeplinkResolutionCallback;
    adjustConfig.cachedAdidReadCallbacks = cachedAdidReadCallbacks;
    adjustConfig.cachedAttributionReadCallbacks = cachedAttributionReadCallbacks;

    activityHandler = AdjustFactory.getActivityHandler(adjustConfig);
    setSendingReferrersAsNotSent(adjustConfig.context);
}

```
[AdjustInstance.java L81-L113](https://github.com/adjust/android_sdk/blob/2fa1a0a4fcaf6adbe045faa9822f0bb46bbf911e/Adjust/sdk-core/src/main/java/com/adjust/sdk/AdjustInstance.java#L81-L113)

不備がある場合、sdkをinitializeせずにreturnするようですね。上から順に読み解いていきましょう。

### AdjustSigner.isPresent()
```java
private static volatile Object signerInstance = null;

public static boolean isPresent() {
    getSignerInstance();
    if (signerInstance != null) {
        return true;
    }
    return false;
}

private static void getSignerInstance() {
    if (signerInstance == null) {
        synchronized (AdjustSigner.class) {
            if (signerInstance == null) {
                signerInstance = Reflection.createDefaultInstance("com.adjust.sdk.sig.Signer");
            }
        }
    }
}
```
[AdjustSigner.java L16-L24](https://github.com/adjust/android_sdk/blob/2fa1a0a4fcaf6adbe045faa9822f0bb46bbf911e/Adjust/sdk-core/src/main/java/com/adjust/sdk/AdjustSigner.java#L16-L24)

ダブルチェックロック方式ですね。読んだらSignerが自動で立ち上がってくれるようです。

### adjustConfig.isValid()
```java
public boolean isValid() {
    if (!checkAppToken(appToken)) {
        return false;
    }
    if (!checkEnvironment(environment)) {
        return false;
    }
    if (!checkContext(context)) {
        return false;
    }
    return true;
}

private boolean checkAppToken(String appToken) {
    if (appToken == null) {
        logger.error("Missing App Token");
        return false;
    }

    if (appToken.length() != 12) {
        logger.error("Malformed App Token '%s'", appToken);
        return false;
    }

    return true;
}

private boolean checkEnvironment(String environment) {
    if (environment == null) {
        logger.error("Missing environment");
        return false;
    }

    if (environment.equals(AdjustConfig.ENVIRONMENT_SANDBOX)) {
        logger.warnInProduction("SANDBOX: Adjust is running in Sandbox mode. " +
                "Use this setting for testing. " +
                "Don't forget to set the environment to `production` before publishing!");
        return true;
    }
    if (environment.equals(AdjustConfig.ENVIRONMENT_PRODUCTION)) {
        logger.warnInProduction(
                "PRODUCTION: Adjust is running in Production mode. " +
                        "Use this setting only for the build that you want to publish. " +
                        "Set the environment to `sandbox` if you want to test your app!");
        return true;
    }

    logger.error("Unknown environment '%s'", environment);
    return false;
}

private boolean checkContext(Context context) {
    if (context == null) {
        logger.error("Missing context");
        return false;
    }

    if (!Util.checkPermission(context, android.Manifest.permission.INTERNET)) {
        logger.error("Missing permission: INTERNET");
        return false;
    }

    return true;
}

```

validateをしていますね。
- いずれも非nullである
- appTokenはString型12文字
- Environmentはsandbox/productionどちらか設定必須
- INTERNETのパーミッションが必要

という条件を満たすとisValidはtrueを返します。

## activityHandler
`activityHandler = AdjustFactory.getActivityHandler(adjustConfig);` で何かをしていそうです。

AdjustFactoryを見に行くと
```java
public static IActivityHandler getActivityHandler(AdjustConfig config) {
    if (activityHandler == null) {
        return ActivityHandler.getInstance(config);
    }
    activityHandler.init(config);
    return activityHandler;
}
```
で、ActivityHandlerを呼び出しています。
ActivityHandlerを覗いてみると
```java
public static ActivityHandler getInstance(AdjustConfig adjustConfig) {
    if (adjustConfig == null) {
        AdjustFactory.getLogger().error("AdjustConfig missing");
        return null;
    }

    if (!adjustConfig.isValid()) {
        AdjustFactory.getLogger().error("AdjustConfig not initialized correctly");
        return null;
    }

    if (adjustConfig.processName != null) {
        int currentPid = android.os.Process.myPid();
        ActivityManager manager = (ActivityManager) adjustConfig.context.getSystemService(Context.ACTIVITY_SERVICE);

        if (manager == null) {
            return null;
        }

        List<ActivityManager.RunningAppProcessInfo> processInfoList = manager.getRunningAppProcesses();
        if (processInfoList == null) {
            return null;
        }

        for (ActivityManager.RunningAppProcessInfo processInfo : processInfoList) {
            if (processInfo.pid == currentPid) {
                if (!processInfo.processName.equalsIgnoreCase(adjustConfig.processName)) {
                    AdjustFactory.getLogger().info("Skipping initialization in background process (%s)", processInfo.processName);
                    return null;
                }
                break;
            }
        }
    }

    ActivityHandler activityHandler = new ActivityHandler(adjustConfig);
    return activityHandler;
}
```
adjustConfigが正しいか確認した後に、プロセスを確認して現在のプロセスと、adjustConfigのprocessNameが等しい場合、ActivityHandlerのインスタンスを作成して返していますね。

コンストラクターを見ると
```java
private ActivityHandler(AdjustConfig adjustConfig) {
    init(adjustConfig);

    // init logger to be available everywhere
    logger = AdjustFactory.getLogger();

    logger.lockLogLevel();

    executor = new SingleThreadCachedScheduler("ActivityHandler");
    internalState = new InternalState();

    // enabled by default
    internalState.enabled = adjustConfig.startEnabled != null ? adjustConfig.startEnabled : true;
    // online by default
    internalState.offline = adjustConfig.startOffline;
    // does not have the session response by default
    internalState.sessionResponseProcessed = false;
    // does not have first start by default
    internalState.firstSdkStart = false;
    // preinstall has not been read by default
    internalState.preinstallHasBeenRead = false;

    executor.submit(() -> initI());
}
public void init(AdjustConfig adjustConfig) {
        this.adjustConfig = adjustConfig;
    }
```
最初にadjustConfigを保存した後、スレッドを立てて処理をしていますね。

このActivityHandler
```java
public class ActivityHandler
  implements IActivityHandler, SystemLifecycleCallback
{
```
とあるようにライフサイクルがあります。主要な箇所を見ていきます。

```java

@Override
public void onResume() {
    onActivityLifecycle(true);
}

@Override
public void onPause() {
    onActivityLifecycle(false);
}


// region SystemLifecycleCallback
@Override public void onActivityLifecycle(final boolean foregroundOrElseBackground) {
    executor.submit(() -> {
        if (internalState.foregroundOrElseBackground != null
          && internalState.foregroundOrElseBackground.booleanValue()
            == foregroundOrElseBackground)
        {
            return;
        }
        // received foregroundOrElseBackground is strictly different from internal state one

        this.internalState.foregroundOrElseBackground = foregroundOrElseBackground;

        if (foregroundOrElseBackground) {
            onResumeI();
        } else {
            onPauseI();
        }
    });
}
```
内部状態が変更された際にonResumeIやonPauseIが実行されそうです。


```java
public void onResumeI() {
    stopBackgroundTimerI();
    startForegroundTimerI();
    logger.verbose("Subsession start");
    startI();
}

    public void onPauseI() {
    stopForegroundTimerI();
    startBackgroundTimerI();
    logger.verbose("Subsession end");
    endI();
}
```
Timerあたりは特に複雑な処理をしていないので読み飛ばして、 `startI` が大切な処理をしています。

```java
private void startI() {
    // check if it's the first sdk start
    if (internalState.hasFirstSdkStartNotOcurred()) {
        AdjustSigner.onResume(adjustConfig.logger);
        startFirstSessionI();
        return;
    } else {
            // check if third party sharing request came, then send it first
            for (AdjustThirdPartySharing adjustThirdPartySharing :
                    adjustConfig.preLaunchActions.preLaunchAdjustThirdPartySharingArray)
            {
                trackThirdPartySharingI(adjustThirdPartySharing);
            }

            if (adjustConfig.preLaunchActions.lastMeasurementConsentTracked != null) {
                trackMeasurementConsentI(
                        adjustConfig.preLaunchActions.
                                lastMeasurementConsentTracked.booleanValue());
            }

            adjustConfig.preLaunchActions.preLaunchAdjustThirdPartySharingArray =
                    new ArrayList<>();
            adjustConfig.preLaunchActions.lastMeasurementConsentTracked = null;
        }

        // it shouldn't start if it was disabled after a first session
        if (!activityState.enabled) {
            return;
        }

        AdjustSigner.onResume(adjustConfig.logger);
        updateHandlersStatusAndSendI();
        processCoppaComplianceI();
        processSessionI();
        checkAttributionStateI();
        processCachedDeeplinkI();
    }
```



とあるように、どうやら初回起動時に `startFirstSessionI` をしているようです。

```java
private void startFirstSessionI() {
    activityState = new ActivityState();
    internalState.firstSdkStart = true;

    activityState.setEventDeduplicationIdsMaxSize(adjustConfig.getEventDeduplicationIdsMaxSize());

    // still update handlers status
    updateHandlersStatusAndSendI();

    long now = System.currentTimeMillis();

    SharedPreferencesManager sharedPreferencesManager = SharedPreferencesManager.getDefaultInstance(getContext());
    activityState.pushToken = sharedPreferencesManager.getPushToken();
    // activityState.isGdprForgotten = sharedPreferencesManager.getGdprForgetMe();

    // track the first session package only if it's enabled
    if (internalState.isEnabled()) {
        if (sharedPreferencesManager.getGdprForgetMe()) {
            gdprForgetMeI();
        } else {
            processCoppaComplianceI();

            // check if third party sharing request came, then send it first
            for (AdjustThirdPartySharing adjustThirdPartySharing :
                    adjustConfig.preLaunchActions.preLaunchAdjustThirdPartySharingArray)
            {
                trackThirdPartySharingI(adjustThirdPartySharing);
            }

            if (adjustConfig.preLaunchActions.lastMeasurementConsentTracked != null) {
                trackMeasurementConsentI(
                        adjustConfig.preLaunchActions.
                                lastMeasurementConsentTracked.booleanValue());
            }

            adjustConfig.preLaunchActions.preLaunchAdjustThirdPartySharingArray =
                    new ArrayList<>();
            adjustConfig.preLaunchActions.lastMeasurementConsentTracked = null;


            activityState.sessionCount = 1; // this is the first session
            transferSessionPackageI(now);
            checkAfterNewStartI(sharedPreferencesManager);
        }
    }

    activityState.resetSessionAttributes(now);
    activityState.enabled = internalState.isEnabled();

    writeActivityStateI();
    sharedPreferencesManager.removePushToken();
    sharedPreferencesManager.removeGdprForgetMe();

    // check for cached deeplinks
    processCachedDeeplinkI();

    // don't check attribution right after first sdk start
}
```

関数名的に `transferSessionPackageI` が送信していそうですね

```java
private void transferSessionPackageI(long now) {
    PackageBuilder builder = new PackageBuilder(adjustConfig, deviceInfo, activityState,
            globalParameters, now);
    builder.internalState = internalState;
    ActivityPackage sessionPackage = builder.buildSessionPackage();
    packageHandler.addPackage(sessionPackage);
    packageHandler.sendFirstPackage();
}
```
ビルダーを使った後、 `sendFirstPackage` してますね

```java
// try to send the oldest package
@Override
public void sendFirstPackage() {
    scheduler.submit(new Runnable() {
        @Override
        public void run() {
            sendFirstI();
        }
    });
}
```
何か送ってますね。

```java
private void sendFirstI() {
    if (packageQueue.isEmpty()) {
        return;
    }

    if (paused) {
        logger.debug("Package handler is paused");
        return;
    }
    if (isSending.getAndSet(true)) {
        logger.verbose("Package handler is already sending");
        return;
    }

    Map<String, String> sendingParameters = generateSendingParametersI();

    ActivityPackage firstPackage = packageQueue.get(0);

    PackageBuilder.addLong(sendingParameters, "retry_count", firstPackage.getRetryCount());
    PackageBuilder.addLong(sendingParameters, "first_error", firstPackage.getFirstErrorCode());
    PackageBuilder.addLong(sendingParameters, "last_error", firstPackage.getLastErrorCode());
    PackageBuilder.addDouble(sendingParameters, "wait_total", totalWaitTimeSeconds);
    PackageBuilder.addDouble(sendingParameters, "wait_time", firstPackage.getWaitBeforeSendTimeSeconds());

    activityPackageSender.sendActivityPackage(firstPackage,
            sendingParameters,
            this);
}
```


```java
@Override
public void sendActivityPackage(final ActivityPackage activityPackage,
                                final Map<String, String> sendingParameters,
                                final ResponseDataCallbackSubscriber responseCallback)
{
    executor.submit(new Runnable() {
        @Override
        public void run() {
            responseCallback.onResponseDataCallback(
                    sendActivityPackageSync(activityPackage, sendingParameters));
        }
    });
}
```

```java
@Override
public ResponseData sendActivityPackageSync(final ActivityPackage activityPackage,
                                            final Map<String, String> sendingParameters)
{
    boolean retryToSend;
    ResponseData responseData;
    do {
        Map<String, String> signedParameters = signParameters(activityPackage, sendingParameters);

        responseData =
                ResponseData.buildResponseData(activityPackage, sendingParameters, signedParameters);

        tryToGetResponse(responseData);

        retryToSend = shouldRetryToSend(responseData);
    } while (retryToSend);

    return responseData;
}
```
retryしまくりますね。実際に通信を行っている箇所がここ

```java
private void tryToGetResponse(final ResponseData responseData) {
    DataOutputStream dataOutputStream = null;

    try {
        String authorizationHeader = extractAuthorizationHeader(responseData.signedParameters);
        logger.verbose("authorizationHeader: %s", authorizationHeader);

        boolean shouldUseGET =
                responseData.activityPackage.getActivityKind() == ActivityKind.ATTRIBUTION;
        final String urlString;
        if (shouldUseGET) {
            urlString = generateUrlStringForGET(responseData.activityPackage.getActivityKind(),
                                                responseData.activityPackage.getPath(),
                                                responseData.activityPackage.getParameters(),
                                                responseData.sendingParameters,
                                                responseData.signedParameters);
        } else {
            urlString = generateUrlStringForPOST(responseData.activityPackage.getActivityKind(),
                                                 responseData.activityPackage.getPath(),
                                                 responseData.signedParameters);
        }

        final URL url = new URL(urlString);
        final HttpsURLConnection connection =
                httpsURLConnectionProvider.generateHttpsURLConnection(url);

        // get and apply connection options (default or for tests)
        connectionOptions.applyConnectionOptions(connection, clientSdk);

        if (authorizationHeader != null) {
            connection.setRequestProperty("Authorization", authorizationHeader);
        }

        if (shouldUseGET) {
            dataOutputStream = configConnectionForGET(connection);
        } else {
            dataOutputStream = configConnectionForPOST(connection,
                                                       responseData.activityPackage.getParameters(),
                                                       responseData.sendingParameters,
                                                       responseData.signedParameters);
        }

        // read connection response
        Integer responseCode = readConnectionResponse(connection, responseData);

        responseData.success =
                responseData.jsonResponse != null
                        && responseData.retryIn == null
                        && responseCode != null
                        && responseCode.intValue() == HttpsURLConnection.HTTP_OK;
        // it is only processed by the server if it contains
        //  a JSON response *AND* does not contain a retry_in
        responseData.willRetry =
                responseData.jsonResponse == null  || responseData.retryIn != null;

        if (responseData.jsonResponse == null) {
            responseData.activityPackage.addError(ErrorCodes.NULL_JSON_RESPONSE);
        } else if (responseData.retryIn != null) {
            responseData.activityPackage.addError(ErrorCodes.SERVER_RETRY_IN);
        }

    } catch (final UnsupportedEncodingException exception) {

        localError(exception, "Failed to encode parameters", responseData, ErrorCodes.UNSUPPORTED_ENCODING_EXCEPTION);

    } catch (final MalformedURLException exception) {

        localError(exception, "Malformed URL", responseData, ErrorCodes.MALFORMED_URL_EXCEPTION);

    } catch (final ProtocolException exception) {

        localError(exception, "Protocol Error", responseData, ErrorCodes.PROTOCOL_EXCEPTION);

    } catch (final SocketTimeoutException exception) {

        // timeout is remote/network related -> did not fail locally
        remoteError(exception, "Request timed out", responseData, ErrorCodes.SOCKET_TIMEOUT_EXCEPTION);

    } catch (final SSLHandshakeException exception) {

        // failed due certificate from the server -> did not fail locally
        remoteError(exception, "Certificate failed", responseData, ErrorCodes.SSL_HANDSHAKE_EXCEPTION);

    } catch (final IOException exception) {

        // IO is the network -> did not fail locally
        remoteError(exception, "Request failed", responseData, ErrorCodes.IO_EXCEPTION);

    } catch (final Throwable t) {

        // not sure if error is local or not -> assume it is local
        localError(t, "Sending SDK package", responseData, ErrorCodes.THROWABLE);

    } finally {
        try {
            if (dataOutputStream != null) {
                dataOutputStream.flush();
                dataOutputStream.close();
            }
        } catch (final IOException ioException) {
            String errorMessage = errorMessage(ioException,
                    "Flushing and closing connection output stream",
                    responseData.activityPackage);
            logger.error(errorMessage);
        }
    }
}

```

というわけで、ここでデータが送信されているんですね。

## setSendingReferrersAsNotSent
initSdkの最後に
`setSendingReferrersAsNotSent(adjustConfig.context);` を呼び出しています。

```java
private void setSendingReferrersAsNotSent(final Context context) {
    SharedPreferencesManager.getDefaultInstance(context).setSendingReferrersAsNotSent();
}
```
前半呼び出しタイミングでsharedPreference読み込んでますね。

```java
private SharedPreferencesManager(final Context context) {
    try {
        sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
        sharedPreferencesEditor = sharedPreferences.edit();
    } catch (Exception exception) {
        AdjustFactory.getLogger().error("Cannot access to SharedPreferences", exception.getMessage());
        sharedPreferences = null;
        sharedPreferencesEditor = null;
    }
}

public static synchronized SharedPreferencesManager getDefaultInstance(final Context context) {
    if (defaultInstance == null) {
        defaultInstance = new SharedPreferencesManager(context);
    }
    return defaultInstance;
}
```

`setSendingReferrersAsNotSent` を見ると

```java
public synchronized void setSendingReferrersAsNotSent() {
    try {
        JSONArray rawReferrerArray = getRawReferrerArray();
        boolean hasRawReferrersBeenChanged = false;
        for (int i = 0; i < rawReferrerArray.length(); i++) {
            JSONArray rawReferrer = rawReferrerArray.getJSONArray(i);
            int sendingStatus = rawReferrer.optInt(INDEX_IS_SENDING, -1);
            if (sendingStatus == 1) {
                rawReferrer.put(INDEX_IS_SENDING, 0);
                hasRawReferrersBeenChanged = true;
            }
        }
        if (hasRawReferrersBeenChanged) {
            saveRawReferrerArray(rawReferrerArray);
        }
    } catch (JSONException e) {
    }
}
public synchronized JSONArray getRawReferrerArray() {
    String referrerQueueString = getString(PREFS_KEY_RAW_REFERRERS);

    if (referrerQueueString != null) {
        try {
            JSONArray rawReferrerArray = new JSONArray(referrerQueueString);

            // Initial move for those who have more than REFERRERS_COUNT stored already.
            // Cut the array and leave it with only REFERRERS_COUNT elements.
            if (rawReferrerArray.length() > REFERRERS_COUNT) {
                JSONArray tempReferrerArray = new JSONArray();
                for (int i = 0; i < REFERRERS_COUNT; i += 1) {
                    tempReferrerArray.put(rawReferrerArray.get(i));
                }
                saveRawReferrerArray(tempReferrerArray);
                return tempReferrerArray;
            }

            return new JSONArray(referrerQueueString);
        } catch (JSONException e) {
        } catch (Throwable t) {
        }
    }

    return new JSONArray();
}
```
String形式で保存しているSharedPreferenceを読み込んでJsonに変換して処理をしていますね。

## インストール計測まとめ
1. `initSdk` を呼び出すと、 `ActivityHandler` が立ち上げる
2. `ActivityHander` のライフサイクル `onResume` で `startI` を呼ぶ
3. `startI` が初めて呼ばれた際 `startFirstSessionI` を呼ぶ
4. 送信するデータを固めた後、成功するまでひたすら送る

# イベント送信
「このユーザーがこのイベントを実行した」みたいな動作を記録する奴です。

```kotlin
val adjustEvent = AdjustEvent("abc123")
Adjust.trackEvent(adjustEvent)
```

非常にシンプルですね。
AdjustEventの引数に利用している `abc123` はイベントトークンと呼ばれ、Adjustのコンソール上で発行することができます。

## AdjustEvent
```java
public AdjustEvent(String eventToken) {
    if (!checkEventToken(eventToken, logger)) return;
    this.eventToken = eventToken;
}

private static boolean checkEventToken(String eventToken, ILogger logger) {
    if (eventToken == null) {
        logger.error("Missing Event Token");
        return false;
    }
    if (eventToken.length() <= 0) {
        logger.error("Event Token can't be empty");
        return false;
    }
    return true;
}
```
特に複雑な事はしていませんね。nullか長さが0だったら早期リターンしています。

イベント収益の記録とかは
```kotlin
adjustEvent.setRevenue(0.01, "EUR")
```

等で設定するそうですが割愛します。

## Adjust.trackEvent
```java
public static void trackEvent(AdjustEvent event) {
    AdjustInstance adjustInstance = Adjust.getDefaultInstance();
    adjustInstance.trackEvent(event);
}
```
initSdkとかで作成したAdjustInstance上で`trackEvent`が実行されそうですね。

```java
public void trackEvent(final AdjustEvent event) {
    if (!checkActivityHandler("trackEvent")) {
        return;
    }
    activityHandler.trackEvent(event);
}
```
何かチェックしていますね。
```java
private boolean checkActivityHandler(final String action) {
    return checkActivityHandler(action, false);
}
private boolean checkActivityHandler(final String action, final boolean actionSaved) {
    if (activityHandler != null) {
        return true;
    }

    if (action == null) {
        AdjustFactory.getLogger().error("Adjust not initialized correctly");
        return false;
    }

    if (actionSaved) {
        AdjustFactory.getLogger().warn(
                "Adjust not initialized, but %s saved for launch",
                action);
    } else {
        AdjustFactory.getLogger().warn(
                "Adjust not initialized, can't perform %s",
                action);
    }
    return false;
}
```
`activityHandler` が無かったらログ出して止めるって事ですね。

`activityHandler.trackEvent(event);` を読み進めます。

```java
@Override
public void trackEvent(final AdjustEvent event) {
    executor.submit(new Runnable() {
        @Override
        public void run() {
            if (internalState.hasFirstSdkStartNotOcurred()) {
                logger.warn("Event tracked before first activity resumed.\n" +
                        "If it was triggered in the Application class, it might timestamp or even send an install long before the user opens the app.\n" +
                        "Please check https://github.com/adjust/android_sdk#can-i-trigger-an-event-at-application-launch for more information.");
                startI();
            }
            trackEventI(event);
        }
    });
}
```

`trackEventI` を呼び出しているようです。

```java
private void trackEventI(AdjustEvent event) {
    if (!checkActivityStateI(activityState)) return;
    if (!isEnabledI()) return;
    if (!checkEventI(event)) return;
    if (activityState.isGdprForgotten) return;
    if (!shouldProcessEventI(event.deduplicationId)) return;

    long now = System.currentTimeMillis();

    activityState.eventCount++;
    updateActivityStateI(now);

    PackageBuilder eventBuilder = new PackageBuilder(adjustConfig, deviceInfo, activityState, globalParameters, now);
    eventBuilder.internalState = internalState;
    ActivityPackage eventPackage = eventBuilder.buildEventPackage(event);
    packageHandler.addPackage(eventPackage);

    packageHandler.sendFirstPackage();

    // if it is in the background and it can send, start the background timer
    if (adjustConfig.isSendingInBackgroundEnabled && internalState.isInBackground()) {
        startBackgroundTimerI();
    }

    writeActivityStateI();
}
```

チェックを通過したらパッケージに詰めて `sendFirstPackage` してますね。

```java
public void addPackage(final ActivityPackage activityPackage) {
    scheduler.submit(new Runnable() {
        @Override
        public void run() {
            addI(activityPackage);
        }
    });
}

private void addI(ActivityPackage newPackage) {
    if (isRetrying) {
        long now = System.currentTimeMillis();
        double waitSeconds = totalWaitTimeSeconds - (now - retryStartedAtTimeMilliSeconds) / 1000.0;;
        newPackage.setWaitBeforeSendTimeSeconds(waitSeconds);
    }
    PackageBuilder.addLong(newPackage.getParameters(), "enqueue_size", packageQueue.size());

    packageQueue.add(newPackage);
    logger.debug("Added package %d (%s)", packageQueue.size(), newPackage);
    logger.verbose("%s", newPackage.getExtendedString());

    writePackageQueueI();
}
```



最後に
```java
private void writeActivityStateI() {
    synchronized (ActivityState.class) {
        if (activityState == null) {
            return;
        }
        Util.writeObject(activityState, adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME);
    }
}

// Util Class
public static <T> void writeObject(T object, Context context, String filename, String objectName) {
    Closeable closable = null;
    try {
        FileOutputStream outputStream = context.openFileOutput(filename, Context.MODE_PRIVATE);
        closable = outputStream;

        BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
        closable = bufferedStream;

        ObjectOutputStream objectStream = new ObjectOutputStream(bufferedStream);
        closable = objectStream;

        try {
            objectStream.writeObject(object);

            getLogger().debug("Wrote %s: %s", objectName, object);
        } catch (NotSerializableException e) {
            getLogger().error("Failed to serialize %s", objectName);
        }
    } catch (Exception e) {
        getLogger().error("Failed to open %s for writing (%s)", objectName, e);
    }
    try {
        if (closable != null) {
            closable.close();
        }
    } catch (Exception e) {
        getLogger().error("Failed to close %s file for writing (%s)", objectName, e);
    }
}
```
で `ActivityState` オブジェクトをシリアライズしてファイルに保存していますね。

# WebView SDK
イベント追加の度にAdjustのイベント追加して、アプリの最新版をリリース というのも面倒ですし、できるだけ全ユーザーのイベントを計測したいですよね？

実はwebViewアプリならAdjust関連の事をJavascriptで呼び出せるので新バージョンのアプリをリリースすることなくイベントを追加できます。

webViewとAdjustを繋ぐ AdjustBridgeを設定する必要があります。

公式で紹介されている手順を掲載します。
先にweb側の導入方法を紹介し、どのような実装になっているのかを見ます。

```javascript
<script type="text/javascript" src="adjust.js"></script>
<script type="text/javascript" src="adjust_event.js"></script>
<script type="text/javascript" src="adjust_third_party_sharing.js"></script>
<script type="text/javascript" src="adjust_config.js"></script>
```
でスクリプトを読み込み

```javascript
let yourAppToken = "{YourAppToken}";
let environment = AdjustConfig.EnvironmentSandbox;
let adjustConfig = new AdjustConfig(yourAppToken, environment);
adjustConfig.setLogLevel(AdjustConfig.LogLevelVerbose);
Adjust.initSdk(adjustConfig);
```
で初回起動計測ができる...と。

このスクリプトファイルを覗くと

```javascript
var Adjust = {
    initSdk: function (adjustConfig) {
        if (adjustConfig && !adjustConfig.getSdkPrefix()) {
            adjustConfig.setSdkPrefix(this.getSdkPrefix());
        }
        this.adjustConfig = adjustConfig;
        if (AdjustBridge) {
            AdjustBridge.initSdk(JSON.stringify(adjustConfig));
        }
    },
}
//省略
```

```javascript
function AdjustConfig(appToken, environment, legacy) {
    if (arguments.length === 2) {
            // new format does not require bridge as first parameter
            this.appToken = appToken;
            this.environment = environment;
        }
    }
}
// 省略       
```
というわけでどうやら `AdjustBridge.initSdk` にjson形式でadjustConfigを渡せばよいみたいです。
後で書きますが `AdjustBridge` はjavascript interfaceです。

web側は上の処理で終了です。
webView SDKを有効化するためにkotlin側は下記のコードを書く必要があります。

```kotlin
class MainActivity : Activity() {
   override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)

         val webView = findViewById<webview>(R.id.webView)
         webView.settings.javaScriptEnabled = true
         webView.webChromeClient = WebChromeClient()
         webView.webViewClient = WebViewClient()

         AdjustBridge.registerAndGetInstance(application, webView)
         try {
            webView.loadUrl("file:///android_asset/AdjustExample-WebView.html")
         } catch (e: Exception) {
            e.printStackTrace()
         }
   }

   override fun onDestroy() {
      AdjustBridge.unregister()

      super.onDestroy()
   }
}
```

要点は
1. `AdjustBridge.registerAndGetInstance(application, webView)` する
2. `onDestroy`時に`AdjustBridge.unregister()` する
です。

早速どんな実装になるか見ていきましょう。

## AdjustBridge
```java
private static AdjustBridgeInstance defaultInstance;

public static synchronized AdjustBridgeInstance registerAndGetInstance(Application application, WebView webView) {
    if (defaultInstance == null) {
        defaultInstance = new AdjustBridgeInstance(application, webView);
    }
    return defaultInstance;
}

public static synchronized AdjustBridgeInstance getDefaultInstance() {
    if (defaultInstance == null) {
        defaultInstance = new AdjustBridgeInstance();
    }
    return defaultInstance;
}
```
もう皆さん見慣れましたね。singletonです

ここで一点皆さんはお気づきかと思いますが
```java
if (defaultInstance == null) {
        defaultInstance = new AdjustBridgeInstance(application, webView);
    }
```
webViewを複数紐づけることができません。webViewの上にFragment載せたり複数のwebView instanceを使う場合はすごい気を付けてください。

```java
private static final String JAVASCRIPT_INTERFACE_NAME = "AdjustBridge";

AdjustBridgeInstance(Application application, WebView webView) {
    this.application = application;
    setWebView(webView);
}

public void setWebView(WebView webView) {
    this.webView = webView;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        webView.addJavascriptInterface(this, JAVASCRIPT_INTERFACE_NAME);
    }
}
```

APIレベル17以上だったら `addJavascriptInterface` してるって事ですね。

web側で`AdjustBridge.initSdk(JSON.stringify(adjustConfig));` を実行すると、AdjustBridgeというjavascript interfaceを介して initSdkが呼ばれるというわけですね。

```java
@JavascriptInterface
public void initSdk(String adjustConfigString) {
    // Initialise SDK only if it's not already initialised.
    if (isInitialized) {
        AdjustBridgeUtil.getLogger().warn("Adjust bridge is already initialized. Ignoring further attempts");
        return;
    }
    if (!isInitialized()) {
        return;
    }

    try {
        AdjustBridgeUtil.getLogger().verbose("Web bridge onCreate adjustConfigString: " + adjustConfigString);
        
        // とても長いので省略. Jsonからデータを読んでるよ
        
        Adjust.initSdk(adjustConfig);
        isInitialized = true;
    } catch (Exception e) {
        AdjustFactory.getLogger().error("AdjustBridgeInstance onCreate: %s", e.getMessage());
    }
}
```
と、要するにAdjust WebView SDKの `Adjust.initSdk(adjustConfig);` はJavascript Interfaceを介してNativeの `Adjust.initSdk(adjustConfig);` を叩いているだけですね。

ちなみに`trackEvent`も
```java
@JavascriptInterface
public void trackEvent(String adjustEventString) {
    if (!isInitialized()) {
        return;
    }

    try {
        JSONObject jsonAdjustEvent = new JSONObject(adjustEventString);

        // とても長いので省略. Jsonからデータを読んでるよ

        // Track event
        Adjust.trackEvent(adjustEvent);
    } catch (Exception e) {
        AdjustFactory.getLogger().error("AdjustBridgeInstance trackEvent: %s", e.getMessage());
    }
}
```
のようにNativeのコードを実行しています。シンプルですね。