Add logging for SettingsPanels

The 3 main categories are:
- Panel Opened
- Panel Closed
- Panel Slice interaction

There are 3 close events:
- See more button
- Done button
- Clicking outside the panel

Slice interactions will log the Uri of the Slice and the ActionType,
such as Toggle, Slider, Content (intent).

Change-Id: Iecad948f39f50dd12ae1bcb23a5d523e0df8bb98
Fixes: 117804231
Test: robotests
This commit is contained in:
Matthew Fritze
2019-02-08 14:09:35 -08:00
parent 545f8b1454
commit b197110f71
15 changed files with 291 additions and 42 deletions

View File

@@ -16,6 +16,7 @@
package com.android.settings.panel;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -64,4 +65,9 @@ public class InternetConnectivityPanel implements PanelContent {
public Intent getSeeMoreIntent() {
return new Intent(Settings.ACTION_WIRELESS_SETTINGS);
}
@Override
public int getMetricsCategory() {
return SettingsEnums.PANEL_INTERNET_CONNECTIVITY;
}
}

View File

@@ -19,6 +19,7 @@ package com.android.settings.panel;
import static com.android.settings.media.MediaOutputSlice.MEDIA_PACKAGE_NAME;
import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -71,4 +72,9 @@ public class MediaOutputPanel implements PanelContent {
public Intent getSeeMoreIntent() {
return null;
}
@Override
public int getMetricsCategory() {
return SettingsEnums.PANEL_MEDIA_OUTPUT;
}
}

View File

@@ -50,4 +50,9 @@ public class NfcPanel implements PanelContent {
intent.setClassName(mContext.getPackageName(), SubSettings.class.getName());
return intent;
}
@Override
public int getMetricsCategory() {
return SettingsEnums.PANEL_NFC;
}
}

View File

@@ -19,12 +19,14 @@ package com.android.settings.panel;
import android.content.Intent;
import android.net.Uri;
import com.android.settingslib.core.instrumentation.Instrumentable;
import java.util.List;
/**
* Represents the data class needed to create a Settings Panel. See {@link PanelFragment}.
*/
public interface PanelContent {
public interface PanelContent extends Instrumentable {
/**
* @return a string for the title of the Panel.

View File

@@ -21,7 +21,8 @@ import android.content.Context;
public interface PanelFeatureProvider {
/**
* Returns {@link PanelContent} as specified by the {@code panelType} and {@code packageName}.
* Returns {@link PanelContent} as specified by the {@param panelType}, and
* {@param mediaPackageName}.
*/
PanelContent getPanel(Context context, String panelType, String packageName);
PanelContent getPanel(Context context, String panelType, String mediaPackageName);
}

View File

@@ -24,7 +24,7 @@ import android.provider.Settings;
public class PanelFeatureProviderImpl implements PanelFeatureProvider {
@Override
public PanelContent getPanel(Context context, String panelType, String packageName) {
public PanelContent getPanel(Context context, String panelType, String mediaPackageName) {
switch (panelType) {
case Settings.Panel.ACTION_INTERNET_CONNECTIVITY:
return InternetConnectivityPanel.create(context);
@@ -33,7 +33,7 @@ public class PanelFeatureProviderImpl implements PanelFeatureProvider {
case Settings.Panel.ACTION_NFC:
return NfcPanel.create(context);
case ACTION_MEDIA_OUTPUT:
return MediaOutputPanel.create(context, packageName);
return MediaOutputPanel.create(context, mediaPackageName);
}
throw new IllegalStateException("No matching panel for: " + panelType);

View File

@@ -16,9 +16,9 @@
package com.android.settings.panel;
import android.content.Intent;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -35,6 +35,8 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.panel.PanelLoggingContract.PanelClosedKeys;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class PanelFragment extends Fragment {
@@ -45,15 +47,15 @@ public class PanelFragment extends Fragment {
private Button mDoneButton;
private RecyclerView mPanelSlices;
private PanelContent mPanel;
private final MetricsFeatureProvider mMetricsProvider;
@VisibleForTesting
PanelSlicesAdapter mAdapter;
private View.OnClickListener mDoneButtonListener = (v) -> {
Log.d(TAG, "Closing dialog");
getActivity().finish();
};
public PanelFragment() {
final Context context = getActivity();
mMetricsProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Nullable
@@ -69,38 +71,70 @@ public class PanelFragment extends Fragment {
mTitleView = view.findViewById(R.id.panel_title);
final Bundle arguments = getArguments();
final String panelType = arguments.getString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT);
final String packageName =
arguments.getString(SettingsPanelActivity.KEY_PANEL_PACKAGE_NAME);
final String panelType =
arguments.getString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT);
final String callingPackageName =
arguments.getString(SettingsPanelActivity.KEY_CALLING_PACKAGE_NAME);
final String mediaPackageName =
arguments.getString(SettingsPanelActivity.KEY_MEDIA_PACKAGE_NAME);
final PanelContent panel = FeatureFactory.getFactory(activity)
// TODO (b/124399577) transform interface to take a context and bundle.
mPanel = FeatureFactory.getFactory(activity)
.getPanelFeatureProvider()
.getPanel(activity, panelType, packageName);
.getPanel(activity, panelType, mediaPackageName);
mAdapter = new PanelSlicesAdapter(this, panel.getSlices());
// Log panel opened.
mMetricsProvider.action(
0 /* attribution */,
SettingsEnums.PAGE_VISIBLE /* opened panel - Action */,
mPanel.getMetricsCategory(),
callingPackageName,
0 /* value */);
mAdapter = new PanelSlicesAdapter(this, mPanel);
mPanelSlices.setHasFixedSize(true);
mPanelSlices.setLayoutManager(new LinearLayoutManager((activity)));
mPanelSlices.setAdapter(mAdapter);
mTitleView.setText(panel.getTitle());
mTitleView.setText(mPanel.getTitle());
mSeeMoreButton.setOnClickListener(getSeeMoreListener(panel.getSeeMoreIntent()));
mDoneButton.setOnClickListener(mDoneButtonListener);
mSeeMoreButton.setOnClickListener(getSeeMoreListener());
mDoneButton.setOnClickListener(getCloseListener());
//If getSeeMoreIntent() is null, hide the mSeeMoreButton.
if (panel.getSeeMoreIntent() == null) {
if (mPanel.getSeeMoreIntent() == null) {
mSeeMoreButton.setVisibility(View.GONE);
}
return view;
}
private View.OnClickListener getSeeMoreListener(final Intent intent) {
@VisibleForTesting
View.OnClickListener getSeeMoreListener() {
return (v) -> {
mMetricsProvider.action(
0 /* attribution */,
SettingsEnums.PAGE_HIDE ,
mPanel.getMetricsCategory(),
PanelClosedKeys.KEY_SEE_MORE,
0 /* value */);
final FragmentActivity activity = getActivity();
activity.startActivity(intent);
activity.startActivityForResult(mPanel.getSeeMoreIntent(), 0);
activity.finish();
};
}
@VisibleForTesting
View.OnClickListener getCloseListener() {
return (v) -> {
mMetricsProvider.action(
0 /* attribution */,
SettingsEnums.PAGE_HIDE,
mPanel.getMetricsCategory(),
PanelClosedKeys.KEY_DONE,
0 /* value */);
getActivity().finish();
};
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.panel;
/**
* Simple contract class to track keys in Panel logging.
*
* <p>
* Constants should only be removed if underlying panel, or use case is removed.
* </p>
*/
public class PanelLoggingContract {
/**
* Keys tracking different ways users exit Panels.
*/
interface PanelClosedKeys {
/**
* The user clicked the See More button linking deeper into Settings.
*/
String KEY_SEE_MORE = "see_more";
/**
* The user clicked the Done button, closing the Panel.
*/
String KEY_DONE = "done";
/**
* The user clicked outside the dialog, closing the Panel.
*/
String KEY_CLICKED_OUT = "clicked_out";
}
}

View File

@@ -16,6 +16,7 @@
package com.android.settings.panel;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
@@ -31,8 +32,8 @@ import androidx.slice.widget.SliceLiveData;
import androidx.slice.widget.SliceView;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import java.util.ArrayList;
import java.util.List;
/**
@@ -43,10 +44,12 @@ public class PanelSlicesAdapter
private final List<Uri> mSliceUris;
private final PanelFragment mPanelFragment;
private final PanelContent mPanelContent;
public PanelSlicesAdapter(PanelFragment fragment, List<Uri> sliceUris) {
public PanelSlicesAdapter(PanelFragment fragment, PanelContent panel) {
mPanelFragment = fragment;
mSliceUris = new ArrayList<>(sliceUris);
mSliceUris = panel.getSlices();
mPanelContent = panel;
}
@NonNull
@@ -56,7 +59,7 @@ public class PanelSlicesAdapter
final LayoutInflater inflater = LayoutInflater.from(context);
final View view = inflater.inflate(R.layout.panel_slice_row, viewGroup, false);
return new SliceRowViewHolder(view);
return new SliceRowViewHolder(view, mPanelContent);
}
@Override
@@ -79,22 +82,38 @@ public class PanelSlicesAdapter
*/
public static class SliceRowViewHolder extends RecyclerView.ViewHolder {
private final PanelContent mPanelContent;
@VisibleForTesting
LiveData<Slice> sliceLiveData;
@VisibleForTesting
final SliceView sliceView;
public SliceRowViewHolder(View view) {
public SliceRowViewHolder(View view, PanelContent panelContent) {
super(view);
sliceView = view.findViewById(R.id.slice_view);
sliceView.setMode(SliceView.MODE_LARGE);
mPanelContent = panelContent;
}
public void onBind(PanelFragment fragment, Uri sliceUri) {
final Context context = sliceView.getContext();
sliceLiveData = SliceLiveData.fromUri(context, sliceUri);
sliceLiveData.observe(fragment.getViewLifecycleOwner(), sliceView);
// Log Panel interaction
sliceView.setOnSliceActionListener(
((eventInfo, sliceItem) -> {
FeatureFactory.getFactory(context)
.getMetricsFeatureProvider()
.action(0 /* attribution */,
SettingsEnums.ACTION_PANEL_INTERACTION,
mPanelContent.getMetricsCategory(),
sliceUri.toString() /* log key */,
eventInfo.actionType /* value */);
})
);
}
}
}

View File

@@ -19,11 +19,13 @@ package com.android.settings.panel;
import static com.android.settingslib.media.MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT;
import static com.android.settingslib.media.MediaOutputSliceConstants.EXTRA_PACKAGE_NAME;
import android.app.settings.SettingsEnums;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.Window;
import android.view.WindowManager;
@@ -34,6 +36,8 @@ import androidx.fragment.app.FragmentManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.panel.PanelLoggingContract.PanelClosedKeys;
/**
* Dialog Activity to host Settings Slices.
@@ -49,7 +53,16 @@ public class SettingsPanelActivity extends FragmentActivity {
* Key specifying which Panel the app is requesting.
*/
public static final String KEY_PANEL_TYPE_ARGUMENT = "PANEL_TYPE_ARGUMENT";
public static final String KEY_PANEL_PACKAGE_NAME = "PANEL_PACKAGE_NAME";
/**
* Key specifying the package which requested the Panel.
*/
public static final String KEY_CALLING_PACKAGE_NAME = "PANEL_CALLING_PACKAGE_NAME";
/**
* Key specifying the package name for which the
*/
public static final String KEY_MEDIA_PACKAGE_NAME = "PANEL_MEDIA_PACKAGE_NAME";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -62,12 +75,12 @@ public class SettingsPanelActivity extends FragmentActivity {
return;
}
final String packageName =
final String mediaPackageName =
callingIntent.getStringExtra(EXTRA_PACKAGE_NAME);
if (TextUtils.equals(ACTION_MEDIA_OUTPUT, callingIntent.getAction())
&& TextUtils.isEmpty(packageName)) {
Log.e(TAG, "Null package name, closing Panel Activity");
&& TextUtils.isEmpty(mediaPackageName)) {
Log.e(TAG, "Missing EXTRA_PACKAGE_NAME, closing Panel Activity");
finish();
return;
}
@@ -81,7 +94,8 @@ public class SettingsPanelActivity extends FragmentActivity {
WindowManager.LayoutParams.WRAP_CONTENT);
mBundle.putString(KEY_PANEL_TYPE_ARGUMENT, callingIntent.getAction());
mBundle.putString(KEY_PANEL_PACKAGE_NAME, packageName);
mBundle.putString(KEY_CALLING_PACKAGE_NAME, getCallingPackage());
mBundle.putString(KEY_MEDIA_PACKAGE_NAME, mediaPackageName);
final PanelFragment panelFragment = new PanelFragment();
panelFragment.setArguments(mBundle);
@@ -92,4 +106,21 @@ public class SettingsPanelActivity extends FragmentActivity {
fragmentManager.beginTransaction().add(R.id.main_content, panelFragment).commit();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
final PanelContent panelContent = FeatureFactory.getFactory(this)
.getPanelFeatureProvider()
.getPanel(this, getIntent().getAction(), null /* Media Package Name */);
FeatureFactory.getFactory(this)
.getMetricsFeatureProvider()
.action(0 /* attribution */,
SettingsEnums.PAGE_HIDE,
panelContent.getMetricsCategory(),
PanelClosedKeys.KEY_CLICKED_OUT,
0 /* value */);
}
return super.onTouchEvent(event);
}
}

View File

@@ -21,6 +21,7 @@ import static com.android.settings.slices.CustomSliceRegistry.VOLUME_CALL_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_MEDIA_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_RINGER_URI;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -62,4 +63,9 @@ public class VolumePanel implements PanelContent {
public Intent getSeeMoreIntent() {
return new Intent(Settings.ACTION_SOUND_SETTINGS);
}
@Override
public int getMetricsCategory() {
return SettingsEnums.PANEL_VOLUME;
}
}

View File

@@ -18,6 +18,7 @@ package com.android.settings.panel;
import static com.android.settings.slices.CustomSliceRegistry.WIFI_SLICE_URI;
import android.app.settings.SettingsEnums;
import android.content.Intent;
import android.net.Uri;
@@ -53,4 +54,9 @@ public class FakePanelContent implements PanelContent {
public Intent getSeeMoreIntent() {
return INTENT;
}
@Override
public int getMetricsCategory() {
return SettingsEnums.TESTING;
}
}

View File

@@ -22,10 +22,13 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import com.android.settings.R;
@@ -71,13 +74,14 @@ public class PanelFragmentTest {
.get()
.getSupportFragmentManager()
.findFragmentById(R.id.main_content));
final Bundle bundle = new Bundle();
bundle.putString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT, FAKE_EXTRA);
doReturn(bundle).when(mPanelFragment).getArguments();
}
@Test
public void onCreateView_adapterGetsDataset() {
final Bundle bundle = new Bundle();
bundle.putString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT, FAKE_EXTRA);
doReturn(bundle).when(mPanelFragment).getArguments();
mPanelFragment.onCreateView(LayoutInflater.from(mContext),
new LinearLayout(mContext), null);
PanelSlicesAdapter adapter = mPanelFragment.mAdapter;
@@ -85,4 +89,43 @@ public class PanelFragmentTest {
assertThat(adapter.getData()).containsAllIn(mFakePanelContent.getSlices());
}
@Test
public void onCreate_logsOpenEvent() {
verify(mFakeFeatureFactory.metricsFeatureProvider).action(
0,
SettingsEnums.PAGE_VISIBLE,
mFakePanelContent.getMetricsCategory(),
null,
0);
}
@Test
public void panelSeeMoreClick_logsCloseEvent() {
final View.OnClickListener listener = mPanelFragment.getSeeMoreListener();
listener.onClick(null);
verify(mFakeFeatureFactory.metricsFeatureProvider).action(
0,
SettingsEnums.PAGE_HIDE,
mFakePanelContent.getMetricsCategory(),
PanelLoggingContract.PanelClosedKeys.KEY_SEE_MORE,
0
);
}
@Test
public void panelDoneClick_logsCloseEvent() {
final View.OnClickListener listener = mPanelFragment.getCloseListener();
listener.onClick(null);
verify(mFakeFeatureFactory.metricsFeatureProvider).action(
0,
SettingsEnums.PAGE_HIDE,
mFakePanelContent.getMetricsCategory(),
PanelLoggingContract.PanelClosedKeys.KEY_DONE,
0
);
}
}

View File

@@ -71,7 +71,7 @@ public class PanelSlicesAdapterTest {
.getSupportFragmentManager()
.findFragmentById(R.id.main_content));
mAdapter = new PanelSlicesAdapter(mPanelFragment, mFakePanelContent.getSlices());
mAdapter = new PanelSlicesAdapter(mPanelFragment, mFakePanelContent);
}
@Test

View File

@@ -16,13 +16,25 @@
package com.android.settings.panel;
import static com.android.settings.panel.SettingsPanelActivity.KEY_PANEL_PACKAGE_NAME;
import static com.android.settings.panel.SettingsPanelActivity.KEY_MEDIA_PACKAGE_NAME;
import static com.android.settings.panel.SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT;
import static com.google.common.truth.Truth.assertThat;
import android.content.Intent;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.settings.SettingsEnums;
import android.content.Intent;
import android.view.MotionEvent;
import com.android.settings.testutils.FakeFeatureFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
@@ -31,6 +43,22 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class SettingsPanelActivityTest {
private FakeFeatureFactory mFakeFeatureFactory;
private FakeSettingsPanelActivity mSettingsPanelActivity;
private PanelFeatureProvider mPanelFeatureProvider;
private FakePanelContent mFakePanelContent;
@Before
public void setUp() {
mFakeFeatureFactory = FakeFeatureFactory.setupForTest();
mSettingsPanelActivity = Robolectric.buildActivity(FakeSettingsPanelActivity.class)
.create().get();
mPanelFeatureProvider = spy(new PanelFeatureProviderImpl());
mFakeFeatureFactory.panelFeatureProvider = mPanelFeatureProvider;
mFakePanelContent = new FakePanelContent();
doReturn(mFakePanelContent).when(mPanelFeatureProvider).getPanel(any(), any(), any());
}
@Test
public void startMediaOutputSlice_withPackageName_bundleShouldHaveValue() {
final Intent intent = new Intent()
@@ -41,7 +69,7 @@ public class SettingsPanelActivityTest {
final SettingsPanelActivity activity =
Robolectric.buildActivity(SettingsPanelActivity.class, intent).create().get();
assertThat(activity.mBundle.getString(KEY_PANEL_PACKAGE_NAME))
assertThat(activity.mBundle.getString(KEY_MEDIA_PACKAGE_NAME))
.isEqualTo("com.google.android.music");
assertThat(activity.mBundle.getString(KEY_PANEL_TYPE_ARGUMENT))
.isEqualTo("com.android.settings.panel.action.MEDIA_OUTPUT");
@@ -55,7 +83,23 @@ public class SettingsPanelActivityTest {
final SettingsPanelActivity activity =
Robolectric.buildActivity(SettingsPanelActivity.class, intent).create().get();
assertThat(activity.mBundle.containsKey(KEY_PANEL_PACKAGE_NAME)).isFalse();
assertThat(activity.mBundle.containsKey(KEY_MEDIA_PACKAGE_NAME)).isFalse();
assertThat(activity.mBundle.containsKey(KEY_PANEL_TYPE_ARGUMENT)).isFalse();
}
@Test
public void onTouchEvent_outsideAction_logsPanelClosed() {
final MotionEvent event = mock(MotionEvent.class);
when(event.getAction()).thenReturn(MotionEvent.ACTION_OUTSIDE);
mSettingsPanelActivity.onTouchEvent(event);
verify(mFakeFeatureFactory.metricsFeatureProvider).action(
0,
SettingsEnums.PAGE_HIDE,
SettingsEnums.TESTING,
PanelLoggingContract.PanelClosedKeys.KEY_CLICKED_OUT,
0
);
}
}