1 /*
2  * Copyright (C) 2018 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  */
16 
17 package com.google.android.setupcompat.logging;
18 
19 import static com.google.android.setupcompat.internal.Validations.assertLengthInRange;
20 
21 import android.annotation.TargetApi;
22 import android.os.Build;
23 import android.os.Build.VERSION_CODES;
24 import android.os.Bundle;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.os.PersistableBundle;
28 import androidx.annotation.NonNull;
29 import androidx.annotation.VisibleForTesting;
30 import com.google.android.setupcompat.internal.ClockProvider;
31 import com.google.android.setupcompat.internal.PersistableBundles;
32 import com.google.android.setupcompat.internal.Preconditions;
33 import com.google.android.setupcompat.util.ObjectUtils;
34 
35 /**
36  * This class represents a interesting event at a particular point in time. The event is identified
37  * by {@link MetricKey} along with {@code timestamp}. It can include additional key-value pairs
38  * providing more attributes associated with the given event. Only primitive values are supported
39  * for now (int, long, double, float, String).
40  */
41 @TargetApi(VERSION_CODES.Q)
42 public final class CustomEvent implements Parcelable {
43   private static final String BUNDLE_KEY_TIMESTAMP = "CustomEvent_timestamp";
44   private static final String BUNDLE_KEY_METRICKEY = "CustomEvent_metricKey";
45   private static final String BUNDLE_KEY_BUNDLE_VALUES = "CustomEvent_bundleValues";
46   private static final String BUNDLE_KEY_BUNDLE_PII_VALUES = "CustomEvent_pii_bundleValues";
47   private static final String BUNDLE_VERSION = "CustomEvent_version";
48   private static final int VERSION = 1;
49 
50   /** Creates a new instance of {@code CustomEvent}. Null arguments are not allowed. */
create( MetricKey metricKey, PersistableBundle bundle, PersistableBundle piiValues)51   public static CustomEvent create(
52       MetricKey metricKey, PersistableBundle bundle, PersistableBundle piiValues) {
53     Preconditions.checkArgument(
54         Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q,
55         "The constructor only support on sdk Q or higher");
56     return new CustomEvent(
57         ClockProvider.timeInMillis(),
58         metricKey,
59         // Assert only in factory methods since these methods are directly used by API consumers
60         // while constructor is used directly only when data is de-serialized from bundle (which
61         // might have been sent by a client using a newer API)
62         PersistableBundles.assertIsValid(bundle),
63         PersistableBundles.assertIsValid(piiValues));
64   }
65 
66   /** Creates a new instance of {@code CustomEvent}. Null arguments are not allowed. */
create(MetricKey metricKey, PersistableBundle bundle)67   public static CustomEvent create(MetricKey metricKey, PersistableBundle bundle) {
68     Preconditions.checkArgument(
69         Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q,
70         "The constructor only support on sdk Q or higher.");
71     return create(metricKey, bundle, PersistableBundle.EMPTY);
72   }
73 
74   /** Converts {@link Bundle} into {@link CustomEvent}. */
toCustomEvent(Bundle bundle)75   public static CustomEvent toCustomEvent(Bundle bundle) {
76     return new CustomEvent(
77         bundle.getLong(BUNDLE_KEY_TIMESTAMP, /* defaultValue= */ Long.MIN_VALUE),
78         MetricKey.toMetricKey(bundle.getBundle(BUNDLE_KEY_METRICKEY)),
79         PersistableBundles.fromBundle(bundle.getBundle(BUNDLE_KEY_BUNDLE_VALUES)),
80         PersistableBundles.fromBundle(bundle.getBundle(BUNDLE_KEY_BUNDLE_PII_VALUES)));
81   }
82 
83   /** Converts {@link CustomEvent} into {@link Bundle}. */
toBundle(CustomEvent customEvent)84   public static Bundle toBundle(CustomEvent customEvent) {
85     Preconditions.checkNotNull(customEvent, "CustomEvent cannot be null");
86     Bundle bundle = new Bundle();
87     bundle.putInt(BUNDLE_VERSION, VERSION);
88     bundle.putLong(BUNDLE_KEY_TIMESTAMP, customEvent.timestampMillis());
89     bundle.putBundle(BUNDLE_KEY_METRICKEY, MetricKey.fromMetricKey(customEvent.metricKey()));
90     bundle.putBundle(BUNDLE_KEY_BUNDLE_VALUES, PersistableBundles.toBundle(customEvent.values()));
91     bundle.putBundle(
92         BUNDLE_KEY_BUNDLE_PII_VALUES, PersistableBundles.toBundle(customEvent.piiValues()));
93     return bundle;
94   }
95 
96   public static final Creator<CustomEvent> CREATOR =
97       new Creator<CustomEvent>() {
98         @Override
99         public CustomEvent createFromParcel(Parcel in) {
100           return new CustomEvent(
101               in.readLong(),
102               in.readParcelable(MetricKey.class.getClassLoader()),
103               in.readPersistableBundle(MetricKey.class.getClassLoader()),
104               in.readPersistableBundle(MetricKey.class.getClassLoader()));
105         }
106 
107         @Override
108         public CustomEvent[] newArray(int size) {
109           return new CustomEvent[size];
110         }
111       };
112 
113   /** Returns the timestamp of when the event occurred. */
timestampMillis()114   public long timestampMillis() {
115     return timestampMillis;
116   }
117 
118   /** Returns the identifier of the event. */
metricKey()119   public MetricKey metricKey() {
120     return this.metricKey;
121   }
122 
123   /** Returns the non PII values describing the event. Only primitive values are supported. */
values()124   public PersistableBundle values() {
125     return new PersistableBundle(this.persistableBundle);
126   }
127 
128   /**
129    * Returns the PII(Personally identifiable information) values describing the event. These values
130    * will not be included in the aggregated logs. Only primitive values are supported.
131    */
piiValues()132   public PersistableBundle piiValues() {
133     return this.piiValues;
134   }
135 
136   @Override
describeContents()137   public int describeContents() {
138     return 0;
139   }
140 
141   @Override
writeToParcel(Parcel parcel, int i)142   public void writeToParcel(Parcel parcel, int i) {
143     parcel.writeLong(timestampMillis);
144     parcel.writeParcelable(metricKey, i);
145     parcel.writePersistableBundle(persistableBundle);
146     parcel.writePersistableBundle(piiValues);
147   }
148 
149   @Override
equals(Object o)150   public boolean equals(Object o) {
151     if (this == o) {
152       return true;
153     }
154     if (!(o instanceof CustomEvent)) {
155       return false;
156     }
157     CustomEvent that = (CustomEvent) o;
158     return timestampMillis == that.timestampMillis
159         && ObjectUtils.equals(metricKey, that.metricKey)
160         && PersistableBundles.equals(persistableBundle, that.persistableBundle)
161         && PersistableBundles.equals(piiValues, that.piiValues);
162   }
163 
164   @Override
hashCode()165   public int hashCode() {
166     return ObjectUtils.hashCode(timestampMillis, metricKey, persistableBundle, piiValues);
167   }
168 
CustomEvent( long timestampMillis, MetricKey metricKey, PersistableBundle bundle, PersistableBundle piiValues)169   private CustomEvent(
170       long timestampMillis,
171       MetricKey metricKey,
172       PersistableBundle bundle,
173       PersistableBundle piiValues) {
174     Preconditions.checkArgument(timestampMillis >= 0, "Timestamp cannot be negative.");
175     Preconditions.checkNotNull(metricKey, "MetricKey cannot be null.");
176     Preconditions.checkNotNull(bundle, "Bundle cannot be null.");
177     Preconditions.checkArgument(!bundle.isEmpty(), "Bundle cannot be empty.");
178     Preconditions.checkNotNull(piiValues, "piiValues cannot be null.");
179     assertPersistableBundleIsValid(bundle);
180     this.timestampMillis = timestampMillis;
181     this.metricKey = metricKey;
182     this.persistableBundle = new PersistableBundle(bundle);
183     this.piiValues = new PersistableBundle(piiValues);
184   }
185 
186   private final long timestampMillis;
187   private final MetricKey metricKey;
188   private final PersistableBundle persistableBundle;
189   private final PersistableBundle piiValues;
190 
assertPersistableBundleIsValid(PersistableBundle bundle)191   private static void assertPersistableBundleIsValid(PersistableBundle bundle) {
192     for (String key : bundle.keySet()) {
193       assertLengthInRange(key, "bundle key", MIN_BUNDLE_KEY_LENGTH, MAX_STR_LENGTH);
194       Object value = bundle.get(key);
195       if (value instanceof String) {
196         Preconditions.checkArgument(
197             ((String) value).length() <= MAX_STR_LENGTH,
198             String.format(
199                 "Maximum length of string value for key='%s' cannot exceed %s.",
200                 key, MAX_STR_LENGTH));
201       }
202     }
203   }
204 
205   /**
206    * Trims the string longer than {@code MAX_STR_LENGTH} character, only keep the first {@code
207    * MAX_STR_LENGTH} - 1 characters and attached … in the end.
208    */
209   @NonNull
trimsStringOverMaxLength(@onNull String str)210   public static String trimsStringOverMaxLength(@NonNull String str) {
211     if (str.length() <= MAX_STR_LENGTH) {
212       return str;
213     } else {
214       return String.format("%s…", str.substring(0, MAX_STR_LENGTH - 1));
215     }
216   }
217 
218   @VisibleForTesting static final int MAX_STR_LENGTH = 50;
219   @VisibleForTesting static final int MIN_BUNDLE_KEY_LENGTH = 3;
220 }
221