1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.settingslib.core.instrumentation;
17
18import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_SETTINGS_PREFERENCE_CHANGE;
19import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_FLOAT_VALUE;
20import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE;
21import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME;
22import static org.mockito.Matchers.any;
23import static org.mockito.Matchers.anyInt;
24import static org.mockito.Matchers.argThat;
25import static org.mockito.Matchers.eq;
26import static org.mockito.Mockito.times;
27import static org.mockito.Mockito.verify;
28
29import android.content.Context;
30import android.content.SharedPreferences;
31import android.util.Pair;
32
33import com.android.settingslib.SettingsLibRobolectricTestRunner;
34
35import org.junit.Before;
36import org.junit.Test;
37import org.junit.runner.RunWith;
38import org.mockito.Answers;
39import org.mockito.ArgumentMatcher;
40import org.mockito.Mock;
41import org.mockito.MockitoAnnotations;
42
43@RunWith(SettingsLibRobolectricTestRunner.class)
44public class SharedPreferenceLoggerTest {
45
46    private static final String TEST_TAG = "tag";
47    private static final String TEST_KEY = "key";
48
49    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
50    private Context mContext;
51
52    private ArgumentMatcher<Pair<Integer, Object>> mNamePairMatcher;
53    @Mock
54    private MetricsFeatureProvider mMetricsFeature;
55    private SharedPreferencesLogger mSharedPrefLogger;
56
57    @Before
58    public void init() {
59        MockitoAnnotations.initMocks(this);
60        mSharedPrefLogger = new SharedPreferencesLogger(mContext, TEST_TAG, mMetricsFeature);
61        mNamePairMatcher = pairMatches(FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, String.class);
62    }
63
64    @Test
65    public void putInt_shouldNotLogInitialPut() {
66        final SharedPreferences.Editor editor = mSharedPrefLogger.edit();
67        editor.putInt(TEST_KEY, 1);
68        editor.putInt(TEST_KEY, 1);
69        editor.putInt(TEST_KEY, 1);
70        editor.putInt(TEST_KEY, 2);
71        editor.putInt(TEST_KEY, 2);
72        editor.putInt(TEST_KEY, 2);
73        editor.putInt(TEST_KEY, 2);
74
75        verify(mMetricsFeature, times(6)).action(any(Context.class), anyInt(),
76                argThat(mNamePairMatcher),
77                argThat(pairMatches(FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE, Integer.class)));
78    }
79
80    @Test
81    public void putBoolean_shouldNotLogInitialPut() {
82        final SharedPreferences.Editor editor = mSharedPrefLogger.edit();
83        editor.putBoolean(TEST_KEY, true);
84        editor.putBoolean(TEST_KEY, true);
85        editor.putBoolean(TEST_KEY, false);
86        editor.putBoolean(TEST_KEY, false);
87        editor.putBoolean(TEST_KEY, false);
88
89
90        verify(mMetricsFeature).action(any(Context.class), anyInt(),
91                argThat(mNamePairMatcher),
92                argThat(pairMatches(FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE, true)));
93        verify(mMetricsFeature, times(3)).action(any(Context.class), anyInt(),
94                argThat(mNamePairMatcher),
95                argThat(pairMatches(FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE, false)));
96    }
97
98    @Test
99    public void putLong_shouldNotLogInitialPut() {
100        final SharedPreferences.Editor editor = mSharedPrefLogger.edit();
101        editor.putLong(TEST_KEY, 1);
102        editor.putLong(TEST_KEY, 1);
103        editor.putLong(TEST_KEY, 1);
104        editor.putLong(TEST_KEY, 1);
105        editor.putLong(TEST_KEY, 2);
106
107        verify(mMetricsFeature, times(4)).action(any(Context.class), anyInt(),
108                argThat(mNamePairMatcher),
109                argThat(pairMatches(FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE, Integer.class)));
110    }
111
112    @Test
113    public void putLong_biggerThanIntMax_shouldLogIntMax() {
114        final SharedPreferences.Editor editor = mSharedPrefLogger.edit();
115        final long veryBigNumber = 500L + Integer.MAX_VALUE;
116        editor.putLong(TEST_KEY, 1);
117        editor.putLong(TEST_KEY, veryBigNumber);
118
119        verify(mMetricsFeature).action(any(Context.class), anyInt(),
120                argThat(mNamePairMatcher),
121                argThat(pairMatches(
122                        FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE, Integer.MAX_VALUE)));
123    }
124
125    @Test
126    public void putLong_smallerThanIntMin_shouldLogIntMin() {
127        final SharedPreferences.Editor editor = mSharedPrefLogger.edit();
128        final long veryNegativeNumber = -500L + Integer.MIN_VALUE;
129        editor.putLong(TEST_KEY, 1);
130        editor.putLong(TEST_KEY, veryNegativeNumber);
131
132        verify(mMetricsFeature).action(any(Context.class), anyInt(),
133                argThat(mNamePairMatcher),
134                argThat(pairMatches(
135                        FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE, Integer.MIN_VALUE)));
136    }
137
138    @Test
139    public void putFloat_shouldNotLogInitialPut() {
140        final SharedPreferences.Editor editor = mSharedPrefLogger.edit();
141        editor.putFloat(TEST_KEY, 1);
142        editor.putFloat(TEST_KEY, 1);
143        editor.putFloat(TEST_KEY, 1);
144        editor.putFloat(TEST_KEY, 1);
145        editor.putFloat(TEST_KEY, 2);
146
147        verify(mMetricsFeature, times(4)).action(any(Context.class), anyInt(),
148                argThat(mNamePairMatcher),
149                argThat(pairMatches(FIELD_SETTINGS_PREFERENCE_CHANGE_FLOAT_VALUE, Float.class)));
150    }
151
152    @Test
153    public void logPackage_shouldUseLogPackageApi() {
154        mSharedPrefLogger.logPackageName("key", "com.android.settings");
155        verify(mMetricsFeature).action(any(Context.class),
156                eq(ACTION_SETTINGS_PREFERENCE_CHANGE),
157                eq("com.android.settings"),
158                any(Pair.class));
159    }
160
161    private ArgumentMatcher<Pair<Integer, Object>> pairMatches(int tag, Class clazz) {
162        return pair -> pair.first == tag && isInstanceOfType(pair.second, clazz);
163    }
164
165    private ArgumentMatcher<Pair<Integer, Object>> pairMatches(int tag, boolean bool) {
166        return pair -> pair.first == tag
167                && isInstanceOfType(pair.second, Integer.class)
168                && pair.second.equals((bool ? 1 : 0));
169    }
170
171    private ArgumentMatcher<Pair<Integer, Object>> pairMatches(int tag, int val) {
172        return pair -> pair.first == tag
173                && isInstanceOfType(pair.second, Integer.class)
174                && pair.second.equals(val);
175    }
176
177    /** Returns true if the instance is assignable to the type Clazz. */
178    private static boolean isInstanceOfType(Object instance, Class<?> clazz) {
179        return clazz.isInstance(instance);
180    }
181}
182