Add SettingsIntelligenceLogwriter

Bug: 124701288
Test: robolectric, manual
Change-Id: Iea446ae600d22ed62c5ee45afd1cd27a89b5de34
diff --git a/Android.mk b/Android.mk
index e385b34..906cfc7 100644
--- a/Android.mk
+++ b/Android.mk
@@ -47,6 +47,7 @@
     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 533dbca..5184218 100644
--- a/protos/Android.bp
+++ b/protos/Android.bp
@@ -5,4 +5,13 @@
         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 0000000..7b28e0d
--- /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 0000000..9498732
--- /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<SettingsLog> 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<Integer, Object>... 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<SettingsLog> 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 93a5163..ec05757 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 @@
     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 b0733f4..727da06 100644
--- a/tests/robotests/Android.mk
+++ b/tests/robotests/Android.mk
@@ -49,6 +49,7 @@
     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 0000000..30a2594
--- /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<SettingsLog> 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);
+    }
+}