From 02e45574a37ce1dae136a9e5393ab839154083e9 Mon Sep 17 00:00:00 2001 From: Raff Tsai Date: Wed, 30 Jan 2019 11:14:20 +0800 Subject: [PATCH] Add SettingsIntelligenceLogwriter Bug: 124701288 Test: robolectric, manual Change-Id: Iea446ae600d22ed62c5ee45afd1cd27a89b5de34 --- Android.mk | 1 + protos/Android.bp | 9 + protos/settings_log_bridge.proto | 37 ++++ .../SettingsIntelligenceLogWriter.java | 190 +++++++++++++++++++++ .../SettingsMetricsFeatureProvider.java | 3 + tests/robotests/Android.mk | 1 + .../SettingsIntelligenceLogWriterTest.java | 78 +++++++++ 7 files changed, 319 insertions(+) create mode 100644 protos/settings_log_bridge.proto create mode 100644 src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java create mode 100644 tests/robotests/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriterTest.java diff --git a/Android.mk b/Android.mk index e385b34051..906cfc7743 100644 --- a/Android.mk +++ b/Android.mk @@ -47,6 +47,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ guava \ jsr305 \ settings-contextual-card-protos-lite \ + settings-log-bridge-protos-lite \ contextualcards \ settings-logtags \ zxing-core-1.7 diff --git a/protos/Android.bp b/protos/Android.bp index 533dbca656..5184218df6 100644 --- a/protos/Android.bp +++ b/protos/Android.bp @@ -5,4 +5,13 @@ java_library_static { type: "lite", }, srcs: ["contextual_card_list.proto"], +} + +java_library_static { + name: "settings-log-bridge-protos-lite", + host_supported: true, + proto: { + type: "lite", + }, + srcs: ["settings_log_bridge.proto"], } \ No newline at end of file diff --git a/protos/settings_log_bridge.proto b/protos/settings_log_bridge.proto new file mode 100644 index 0000000000..7b28e0d0d0 --- /dev/null +++ b/protos/settings_log_bridge.proto @@ -0,0 +1,37 @@ +syntax = "proto2"; + +package com.android.settings.intelligence; +option java_outer_classname = "LogProto"; + +message SettingsLog { + /** + * Where this SettingsUIChange event comes from. For example, if + * it's a PAGE_VISIBLE event, where the page is opened from. + */ + optional int32 attribution = 1; + + /** + * What the UI action is. + */ + optional int32 action = 2; + + /** + * Where the action is happening + */ + optional int32 page_id = 3; + + /** + * What preference changed in this event. + */ + optional string changed_preference_key = 4; + + /** + * The new value of the changed preference. + */ + optional int32 changed_preference_int_value = 5; + + /** + * The timestamp of a log event + */ + optional string timestamp = 6; +} diff --git a/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java b/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java new file mode 100644 index 0000000000..9498732e52 --- /dev/null +++ b/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.core.instrumentation; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Process; +import android.os.UserHandle; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.intelligence.LogProto.SettingsLog; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.LogWriter; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.LinkedList; +import java.util.List; + +public class SettingsIntelligenceLogWriter implements LogWriter { + private static final String TAG = "IntelligenceLogWriter"; + + private static final String LOG = "logs"; + private static final long MESSAGE_DELAY = DateUtils.MINUTE_IN_MILLIS; // 1 minute + + private List mSettingsLogList; + private SendLogHandler mLogHandler; + + public SettingsIntelligenceLogWriter() { + mSettingsLogList = new LinkedList<>(); + final HandlerThread workerThread = new HandlerThread("SettingsIntelligenceLogWriter", + Process.THREAD_PRIORITY_BACKGROUND); + workerThread.start(); + mLogHandler = new SendLogHandler(workerThread.getLooper()); + } + + @Override + public void visible(Context context, int attribution, int pageId) { + action(attribution /* attribution */, + SettingsEnums.PAGE_VISIBLE /* action */, + pageId /* pageId */, + "" /* changedPreferenceKey */, + 0 /* changedPreferenceIntValue */); + } + + @Override + public void hidden(Context context, int pageId) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + SettingsEnums.PAGE_HIDE /* action */, + pageId /* pageId */, + "" /* changedPreferenceKey */, + 0 /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, Pair... taggedData) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + "" /* changedPreferenceKey */, + 0 /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, int value) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + "" /* changedPreferenceKey */, + value /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, boolean value) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + "" /* changedPreferenceKey */, + value ? 1 : 0 /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, String pkg) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + pkg /* changedPreferenceKey */, + 1 /* changedPreferenceIntValue */); + } + + @Override + public void action(int attribution, int action, int pageId, String key, int value) { + final ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); + final SettingsLog settingsLog = SettingsLog.newBuilder() + .setAttribution(attribution) + .setAction(action) + .setPageId(pageId) + .setChangedPreferenceKey(key != null ? key : "") + .setChangedPreferenceIntValue(value) + .setTimestamp(now.toString()) + .build(); + mLogHandler.post(() -> { + mSettingsLogList.add(settingsLog); + }); + mLogHandler.scheduleSendLog(); + } + + @VisibleForTesting + static byte[] serialize(List settingsLogs) { + final int size = settingsLogs.size(); + final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + final DataOutputStream output = new DataOutputStream(bout); + // Data is "size, length, bytearray, length, bytearray ..." + try { + output.writeInt(size); + for (SettingsLog settingsLog : settingsLogs) { + final byte[] data = settingsLog.toByteArray(); + output.writeInt(data.length); + output.write(data); + } + return bout.toByteArray(); + } catch (Exception e) { + Log.e(TAG, "serialize error", e); + return null; + } finally { + try { + output.close(); + } catch (Exception e) { + Log.e(TAG, "close error", e); + } + } + } + + private class SendLogHandler extends Handler { + + public SendLogHandler(Looper looper) { + super(looper); + } + + public void scheduleSendLog() { + removeCallbacks(mSendLogsRunnable); + postDelayed(mSendLogsRunnable, MESSAGE_DELAY); + } + } + + private final Runnable mSendLogsRunnable = () -> { + final Context context = FeatureFactory.getAppContext(); + if (context == null) { + Log.e(TAG, "context is null"); + return; + } + final String action = context.getString(R.string + .config_settingsintelligence_log_action); + if (!TextUtils.isEmpty(action) && !mSettingsLogList.isEmpty()) { + final Intent intent = new Intent(); + intent.setPackage(context.getString(R.string + .config_settingsintelligence_package_name)); + intent.setAction(action); + intent.putExtra(LOG, serialize(mSettingsLogList)); + context.sendBroadcastAsUser(intent, UserHandle.CURRENT); + mSettingsLogList.clear(); + } + }; +} diff --git a/src/com/android/settings/core/instrumentation/SettingsMetricsFeatureProvider.java b/src/com/android/settings/core/instrumentation/SettingsMetricsFeatureProvider.java index 93a5163645..ec05757205 100644 --- a/src/com/android/settings/core/instrumentation/SettingsMetricsFeatureProvider.java +++ b/src/com/android/settings/core/instrumentation/SettingsMetricsFeatureProvider.java @@ -16,6 +16,8 @@ package com.android.settings.core.instrumentation; +import android.content.Context; + import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; public class SettingsMetricsFeatureProvider extends MetricsFeatureProvider { @@ -23,5 +25,6 @@ public class SettingsMetricsFeatureProvider extends MetricsFeatureProvider { protected void installLogWriters() { super.installLogWriters(); mLoggerWriters.add(new StatsLogWriter()); + mLoggerWriters.add(new SettingsIntelligenceLogWriter()); } } diff --git a/tests/robotests/Android.mk b/tests/robotests/Android.mk index b0733f4ded..727da064a5 100644 --- a/tests/robotests/Android.mk +++ b/tests/robotests/Android.mk @@ -49,6 +49,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ guava \ jsr305 \ settings-contextual-card-protos-lite \ + settings-log-bridge-protos-lite \ contextualcards \ settings-logtags \ zxing-core-1.7 diff --git a/tests/robotests/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriterTest.java b/tests/robotests/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriterTest.java new file mode 100644 index 0000000000..30a25945d2 --- /dev/null +++ b/tests/robotests/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriterTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.core.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.settings.SettingsEnums; +import android.content.Context; + +import com.android.settings.intelligence.LogProto.SettingsLog; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class SettingsIntelligenceLogWriterTest { + private Context mContext; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + } + + @Test + public void serialize_hasSizeOne_returnCorrectData() throws IOException { + final SettingsLog event = SettingsLog.newBuilder() + .setAttribution(SettingsEnums.DASHBOARD_SUMMARY) + .setAction(SettingsEnums.ACTION_SET_NEW_PASSWORD) + .setPageId(SettingsEnums.SET_NEW_PASSWORD_ACTIVITY) + .setChangedPreferenceKey("package") + .setChangedPreferenceIntValue(100) + .build(); + List events = new ArrayList<>(); + events.add(event); + + // execute + final byte[] data = SettingsIntelligenceLogWriter.serialize(events); + + // parse data + final ByteArrayInputStream bin = new ByteArrayInputStream(data); + final DataInputStream inputStream = new DataInputStream(bin); + final int size = inputStream.readInt(); + final byte[] change = new byte[inputStream.readInt()]; + inputStream.read(change); + inputStream.close(); + final SettingsLog settingsLog = SettingsLog.parseFrom(change); + + // assert + assertThat(events.size()).isEqualTo(size); + assertThat(settingsLog.getAttribution()).isEqualTo(SettingsEnums.DASHBOARD_SUMMARY); + assertThat(settingsLog.getAction()).isEqualTo(SettingsEnums.ACTION_SET_NEW_PASSWORD); + assertThat(settingsLog.getPageId()).isEqualTo(SettingsEnums.SET_NEW_PASSWORD_ACTIVITY); + assertThat(settingsLog.getChangedPreferenceKey()).isEqualTo("package"); + assertThat(settingsLog.getChangedPreferenceIntValue()).isEqualTo(100); + } +} -- 2.11.0