1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Adjust SDK v5を読み解く[Android]

Posted at

この記事は何?

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

Adjustとは

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

(公式サイトより引用)

実際に楽天アプリを使ってかみ砕いて説明すると、楽天市場サービス一覧にあるような
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
SDK v5移行ガイド Android

今回はSDK v5のAdjuts SDK Android版を見ます
見るレポジトリ: adjust/adjust_sdk
見るtag: v5.0.0

本編

進め方

連携ガイドを元に、このコードを打つとどう動くんだ?を読み解いていきます。

インストール計測

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

AdjustConfig(this, appToken, environment) を呼び出すとコレが呼び出されます。

public AdjustConfig(Context context, String appToken, String environment) {
    init(context, appToken, environment, false);
}

自前のinit関数が呼ばれます。第四引数がfalseで固定ですね。何でしょうコレ。見てみましょう

private void init(Context context, String appToken, String environment, boolean allowSuppressLogLevel)

とあるように、 allowSuppressLogLevel が常時falseになるようです。ログ消したい人はコッチを使った方が良いですね。

public AdjustConfig(Context context, String appToken, String environment, boolean allowSuppressLogLevel) {
    init(context, appToken, environment, allowSuppressLogLevel);
}

AdjustConfig init

話がそれました。init関数を読み解いていきます。

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

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

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

AdjustFactoryを見に行くとloggerは

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

private void setLogLevel(LogLevel logLevel, String environment) {
    logger.setLogLevel(logLevel, AdjustConfig.ENVIRONMENT_PRODUCTION.equals(environment));
}

なので特に説明は不要かと思います。

Adjust initSdk

Adjust.initSdk(config)

でAdjustの初期化をしています。
Adjust.java

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

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

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で処理を実行しているようです。読み解いていきましょう。

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

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

AdjustSigner.isPresent()

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

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

adjustConfig.isValid()

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を見に行くと

public static IActivityHandler getActivityHandler(AdjustConfig config) {
    if (activityHandler == null) {
        return ActivityHandler.getInstance(config);
    }
    activityHandler.init(config);
    return activityHandler;
}

で、ActivityHandlerを呼び出しています。
ActivityHandlerを覗いてみると

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のインスタンスを作成して返していますね。

コンストラクターを見ると

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

public class ActivityHandler
  implements IActivityHandler, SystemLifecycleCallback
{

とあるようにライフサイクルがあります。主要な箇所を見ていきます。


@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が実行されそうです。

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

    public void onPauseI() {
    stopForegroundTimerI();
    startBackgroundTimerI();
    logger.verbose("Subsession end");
    endI();
}

Timerあたりは特に複雑な処理をしていないので読み飛ばして、 startI が大切な処理をしています。

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 をしているようです。

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 が送信していそうですね

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 してますね

// try to send the oldest package
@Override
public void sendFirstPackage() {
    scheduler.submit(new Runnable() {
        @Override
        public void run() {
            sendFirstI();
        }
    });
}

何か送ってますね。

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);
}
@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));
        }
    });
}
@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しまくりますね。実際に通信を行っている箇所がここ

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); を呼び出しています。

private void setSendingReferrersAsNotSent(final Context context) {
    SharedPreferencesManager.getDefaultInstance(context).setSendingReferrersAsNotSent();
}

前半呼び出しタイミングでsharedPreference読み込んでますね。

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 を見ると

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 のライフサイクル onResumestartI を呼ぶ
  3. startI が初めて呼ばれた際 startFirstSessionI を呼ぶ
  4. 送信するデータを固めた後、成功するまでひたすら送る

イベント送信

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

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

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

AdjustEvent

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だったら早期リターンしています。

イベント収益の記録とかは

adjustEvent.setRevenue(0.01, "EUR")

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

Adjust.trackEvent

public static void trackEvent(AdjustEvent event) {
    AdjustInstance adjustInstance = Adjust.getDefaultInstance();
    adjustInstance.trackEvent(event);
}

initSdkとかで作成したAdjustInstance上でtrackEventが実行されそうですね。

public void trackEvent(final AdjustEvent event) {
    if (!checkActivityHandler("trackEvent")) {
        return;
    }
    activityHandler.trackEvent(event);
}

何かチェックしていますね。

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); を読み進めます。

@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 を呼び出しているようです。

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 してますね。

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();
}

最後に

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側の導入方法を紹介し、どのような実装になっているのかを見ます。

<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>

でスクリプトを読み込み

let yourAppToken = "{YourAppToken}";
let environment = AdjustConfig.EnvironmentSandbox;
let adjustConfig = new AdjustConfig(yourAppToken, environment);
adjustConfig.setLogLevel(AdjustConfig.LogLevelVerbose);
Adjust.initSdk(adjustConfig);

で初回起動計測ができる...と。

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

var Adjust = {
    initSdk: function (adjustConfig) {
        if (adjustConfig && !adjustConfig.getSdkPrefix()) {
            adjustConfig.setSdkPrefix(this.getSdkPrefix());
        }
        this.adjustConfig = adjustConfig;
        if (AdjustBridge) {
            AdjustBridge.initSdk(JSON.stringify(adjustConfig));
        }
    },
}
//省略
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側は下記のコードを書く必要があります。

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

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です

ここで一点皆さんはお気づきかと思いますが

if (defaultInstance == null) {
        defaultInstance = new AdjustBridgeInstance(application, webView);
    }

webViewを複数紐づけることができません。webViewの上にFragment載せたり複数のwebView instanceを使う場合はすごい気を付けてください。

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が呼ばれるというわけですね。

@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

@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のコードを実行しています。シンプルですね。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?