この記事は何?
Adjustのコードを読んで何となく語学の為にメモしたものです。
長いので生成AIに読み込ませて要約して貰って適宜質問を投げるといった使い方が良いかと思います。
Adjustとは
Adjustは、アプリの最適化とグロースを支援するソリューションです。
複数のプラットフォームを横断する計測技術で、アプリマーケティングの全過程においてマーケターをサポートします。
(公式サイトより引用)
実際に楽天アプリを使ってかみ砕いて説明すると、楽天市場サービス一覧にあるような
https://app.adjust.com/dj7tmxv のようなadjustリンクを踏んだ際に
- Adjustがリンクをクリックしたことを記録
- iOSはapp storeへ、androidはGoogle Play Storeにリダイレクトする
- アプリを初回起動時にインストールされたことをAdjustに送信
- 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;
}
複雑な事はしていませんね。
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);
}
不備がある場合、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");
}
}
}
}
ダブルチェックロック方式ですね。読んだら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に変換して処理をしていますね。
インストール計測まとめ
-
initSdk
を呼び出すと、ActivityHandler
が立ち上げる -
ActivityHander
のライフサイクルonResume
でstartI
を呼ぶ -
startI
が初めて呼ばれた際startFirstSessionI
を呼ぶ - 送信するデータを固めた後、成功するまでひたすら送る
イベント送信
「このユーザーがこのイベントを実行した」みたいな動作を記録する奴です。
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()
}
}
要点は
-
AdjustBridge.registerAndGetInstance(application, webView)
する -
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のコードを実行しています。シンプルですね。