1 /*
2  * Copyright (C) 2022 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.android.ondevicepersonalization.services.data.user;
18 
19 import static android.content.pm.PackageManager.GET_META_DATA;
20 
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.PackageManager;
26 import android.content.res.Configuration;
27 import android.net.ConnectivityManager;
28 import android.net.NetworkCapabilities;
29 import android.os.BatteryManager;
30 import android.os.Environment;
31 import android.os.StatFs;
32 import android.telephony.TelephonyManager;
33 
34 import androidx.annotation.NonNull;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.odp.module.common.MonotonicClock;
38 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
39 import com.android.ondevicepersonalization.services.FlagsFactory;
40 
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Locale;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.TimeZone;
47 
48 /**
49  * A collector for getting user data signals. This class only exposes two public operations:
50  * periodic update, and real-time update. Periodic update operation will be run every 4 hours in the
51  * background, given several on-device resource constraints are satisfied. Real-time update
52  * operation will be run before any ads serving request and update a few time-sensitive signals in
53  * UserData to the latest version.
54  */
55 public class UserDataCollector {
56     private static final int MILLISECONDS_IN_MINUTE = 60000;
57 
58     private static volatile UserDataCollector sUserDataCollector = null;
59     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
60     private static final String TAG = UserDataCollector.class.getSimpleName();
61 
62     @VisibleForTesting
63     public static final Set<Integer> ALLOWED_NETWORK_TYPE =
64             Set.of(
65                     TelephonyManager.NETWORK_TYPE_UNKNOWN,
66                     TelephonyManager.NETWORK_TYPE_GPRS,
67                     TelephonyManager.NETWORK_TYPE_EDGE,
68                     TelephonyManager.NETWORK_TYPE_UMTS,
69                     TelephonyManager.NETWORK_TYPE_CDMA,
70                     TelephonyManager.NETWORK_TYPE_EVDO_0,
71                     TelephonyManager.NETWORK_TYPE_EVDO_A,
72                     TelephonyManager.NETWORK_TYPE_1xRTT,
73                     TelephonyManager.NETWORK_TYPE_HSDPA,
74                     TelephonyManager.NETWORK_TYPE_HSUPA,
75                     TelephonyManager.NETWORK_TYPE_HSPA,
76                     TelephonyManager.NETWORK_TYPE_EVDO_B,
77                     TelephonyManager.NETWORK_TYPE_LTE,
78                     TelephonyManager.NETWORK_TYPE_EHRPD,
79                     TelephonyManager.NETWORK_TYPE_HSPAP,
80                     TelephonyManager.NETWORK_TYPE_GSM,
81                     TelephonyManager.NETWORK_TYPE_TD_SCDMA,
82                     TelephonyManager.NETWORK_TYPE_IWLAN,
83                     TelephonyManager.NETWORK_TYPE_NR);
84 
85     @NonNull private final Context mContext;
86     @NonNull private final TelephonyManager mTelephonyManager;
87     @NonNull final ConnectivityManager mConnectivityManager;
88     // Metadata to track whether UserData has been initialized.
89     @NonNull private boolean mInitialized;
90     private final UserDataDao mUserDataDao;
91 
UserDataCollector(Context context, UserDataDao userDataDao)92     private UserDataCollector(Context context, UserDataDao userDataDao) {
93         mContext = context;
94         mUserDataDao = userDataDao;
95         mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
96         mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
97         mInitialized = false;
98     }
99 
100     /** Returns an instance of UserDataCollector. */
getInstance(Context context)101     public static UserDataCollector getInstance(Context context) {
102         if (sUserDataCollector == null) {
103             synchronized (UserDataCollector.class) {
104                 if (sUserDataCollector == null) {
105                     sUserDataCollector =
106                             new UserDataCollector(
107                                     context.getApplicationContext(),
108                                     UserDataDao.getInstance(context));
109                 }
110             }
111         }
112         return sUserDataCollector;
113     }
114 
115     /**
116      * Returns an instance of the UserDataCollector which is not a singleton instance. It's only for
117      * testing purpose.
118      */
119     @VisibleForTesting
getInstanceForTest(Context context, UserDataDao userDataDao)120     public static UserDataCollector getInstanceForTest(Context context, UserDataDao userDataDao) {
121         return new UserDataCollector(context, userDataDao);
122     }
123 
124     /** Returns a singleton instance of the UserDataCollector. It's only for testing purpose. */
125     @VisibleForTesting
getInstanceForTest(Context context)126     public static UserDataCollector getInstanceForTest(Context context) {
127         synchronized (UserDataCollector.class) {
128             if (sUserDataCollector == null) {
129                 sUserDataCollector =
130                         new UserDataCollector(context, UserDataDao.getInstanceForTest(context));
131             }
132             return sUserDataCollector;
133         }
134     }
135 
136     /** Update real-time user data to the latest per request. */
getRealTimeData(@onNull RawUserData userData)137     public void getRealTimeData(@NonNull RawUserData userData) {
138         /**
139          * Ads serving requires real-time latency. If user data has not been initialized, we will
140          * skip user data collection for the incoming request and wait until the first {@link
141          * UserDataCollectionJobService} to be scheduled.
142          */
143         if (!mInitialized) {
144             return;
145         }
146         getUtcOffset(userData);
147         getOrientation(userData);
148     }
149 
150     /** Update user data per periodic job servce. */
updateUserData(@onNull RawUserData userData)151     public void updateUserData(@NonNull RawUserData userData) {
152         if (!mInitialized) {
153             initializeUserData(userData);
154             return;
155         }
156         getAvailableStorageBytes(userData);
157         getBatteryPercentage(userData);
158         getCarrier(userData);
159         getNetworkCapabilities(userData);
160         getDataNetworkType(userData);
161         updateInstalledApps(userData);
162     }
163 
164     /**
165      * Collects in-memory user data signals and stores in a UserData object for the schedule of
166      * {@link UserDataCollectionJobService}
167      */
initializeUserData(@onNull RawUserData userData)168     private void initializeUserData(@NonNull RawUserData userData) {
169         getUtcOffset(userData);
170         getOrientation(userData);
171         getAvailableStorageBytes(userData);
172         getBatteryPercentage(userData);
173         getCarrier(userData);
174         getNetworkCapabilities(userData);
175         getDataNetworkType(userData);
176         initialInstalledApp(userData);
177 
178         mInitialized = true;
179     }
180 
181     /** Collects current device's time zone in +/- offset of minutes from UTC. */
182     @VisibleForTesting
getUtcOffset(RawUserData userData)183     public void getUtcOffset(RawUserData userData) {
184         try {
185             userData.utcOffset =
186                     TimeZone.getDefault().getOffset(System.currentTimeMillis())
187                             / MILLISECONDS_IN_MINUTE;
188         } catch (Exception e) {
189             sLogger.w(TAG + ": Failed to collect timezone offset.", e);
190         }
191     }
192 
193     /** Collects the current device orientation. */
194     @VisibleForTesting
getOrientation(RawUserData userData)195     public void getOrientation(RawUserData userData) {
196         try {
197             userData.orientation = mContext.getResources().getConfiguration().orientation;
198         } catch (Exception e) {
199             sLogger.w(TAG + ": Failed to collect device orientation.", e);
200         }
201     }
202 
203     /** Collects available bytes and converts to MB. */
204     @VisibleForTesting
getAvailableStorageBytes(RawUserData userData)205     public void getAvailableStorageBytes(RawUserData userData) {
206         try {
207             StatFs statFs = new StatFs(Environment.getDataDirectory().getPath());
208             userData.availableStorageBytes = statFs.getAvailableBytes();
209         } catch (Exception e) {
210             sLogger.w(TAG + ": Failed to collect availableStorageBytes.", e);
211         }
212     }
213 
214     /** Collects the battery percentage of the device. */
215     @VisibleForTesting
getBatteryPercentage(RawUserData userData)216     public void getBatteryPercentage(RawUserData userData) {
217         try {
218             IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
219             Intent batteryStatus = mContext.registerReceiver(null, ifilter);
220 
221             int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
222             int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
223             if (level >= 0 && scale > 0) {
224                 userData.batteryPercentage = Math.round(level * 100.0f / (float) scale);
225             }
226         } catch (Exception e) {
227             sLogger.w(TAG + ": Failed to collect batteryPercentage.", e);
228         }
229     }
230 
231     /** Collects carrier info. */
232     @VisibleForTesting
getCarrier(RawUserData userData)233     public void getCarrier(RawUserData userData) {
234         // TODO (b/307158231): handle i18n later if the carrier's name is in non-English script.
235         try {
236             switch (mTelephonyManager.getSimOperatorName().toUpperCase(Locale.US)) {
237                 case "RELIANCE JIO" -> userData.carrier = Carrier.RELIANCE_JIO;
238                 case "VODAFONE" -> userData.carrier = Carrier.VODAFONE;
239                 case "T-MOBILE - US", "T-MOBILE" -> userData.carrier = Carrier.T_MOBILE;
240                 case "VERIZON WIRELESS" -> userData.carrier = Carrier.VERIZON_WIRELESS;
241                 case "AIRTEL" -> userData.carrier = Carrier.AIRTEL;
242                 case "ORANGE" -> userData.carrier = Carrier.ORANGE;
243                 case "NTT DOCOMO" -> userData.carrier = Carrier.NTT_DOCOMO;
244                 case "MOVISTAR" -> userData.carrier = Carrier.MOVISTAR;
245                 case "AT&T" -> userData.carrier = Carrier.AT_T;
246                 case "TELCEL" -> userData.carrier = Carrier.TELCEL;
247                 case "VIVO" -> userData.carrier = Carrier.VIVO;
248                 case "VI" -> userData.carrier = Carrier.VI;
249                 case "TIM" -> userData.carrier = Carrier.TIM;
250                 case "O2" -> userData.carrier = Carrier.O2;
251                 case "TELEKOM" -> userData.carrier = Carrier.TELEKOM;
252                 case "CLARO BR" -> userData.carrier = Carrier.CLARO_BR;
253                 case "SK TELECOM" -> userData.carrier = Carrier.SK_TELECOM;
254                 case "MTC" -> userData.carrier = Carrier.MTC;
255                 case "AU" -> userData.carrier = Carrier.AU;
256                 case "TELE2" -> userData.carrier = Carrier.TELE2;
257                 case "SFR" -> userData.carrier = Carrier.SFR;
258                 case "ETECSA" -> userData.carrier = Carrier.ETECSA;
259                 case "IR-MCI (HAMRAHE AVVAL)" -> userData.carrier = Carrier.IR_MCI;
260                 case "KT" -> userData.carrier = Carrier.KT;
261                 case "TELKOMSEL" -> userData.carrier = Carrier.TELKOMSEL;
262                 case "IRANCELL" -> userData.carrier = Carrier.IRANCELL;
263                 case "MEGAFON" -> userData.carrier = Carrier.MEGAFON;
264                 case "TELEFONICA" -> userData.carrier = Carrier.TELEFONICA;
265                 default -> userData.carrier = Carrier.UNKNOWN;
266             }
267         } catch (Exception e) {
268             sLogger.w(TAG + "Failed to collect carrier info.", e);
269         }
270     }
271 
272     /** Collects network capabilities. */
273     @VisibleForTesting
getNetworkCapabilities(RawUserData userData)274     public void getNetworkCapabilities(RawUserData userData) {
275         try {
276             NetworkCapabilities networkCapabilities =
277                     mConnectivityManager.getNetworkCapabilities(
278                             mConnectivityManager.getActiveNetwork());
279             userData.networkCapabilities = getFilteredNetworkCapabilities(networkCapabilities);
280         } catch (Exception e) {
281             sLogger.w(TAG + ": Failed to collect networkCapabilities.", e);
282         }
283     }
284 
285     @VisibleForTesting
getDataNetworkType(RawUserData userData)286     public void getDataNetworkType(RawUserData userData) {
287         try {
288             int dataNetworkType = mTelephonyManager.getDataNetworkType();
289             if (!ALLOWED_NETWORK_TYPE.contains(dataNetworkType)) {
290                 userData.dataNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN;
291             } else {
292                 userData.dataNetworkType = dataNetworkType;
293             }
294         } catch (Exception e) {
295             sLogger.w(TAG + ": Failed to collect data network type.", e);
296         }
297     }
298 
299     /** Util to reset all fields in [UserData] to default for testing purpose */
clearUserData(@onNull RawUserData userData)300     public void clearUserData(@NonNull RawUserData userData) {
301         userData.utcOffset = 0;
302         userData.orientation = Configuration.ORIENTATION_PORTRAIT;
303         userData.availableStorageBytes = 0;
304         userData.batteryPercentage = 0;
305         userData.carrier = Carrier.UNKNOWN;
306         userData.networkCapabilities = null;
307         userData.installedApps.clear();
308     }
309 
310     /** Util to reset all in-memory metadata for testing purpose. */
clearMetadata()311     public void clearMetadata() {
312         mInitialized = false;
313     }
314 
315     @VisibleForTesting
isInitialized()316     public boolean isInitialized() {
317         return mInitialized;
318     }
319 
320     @VisibleForTesting
getFilteredNetworkCapabilities( NetworkCapabilities networkCapabilities)321     static NetworkCapabilities getFilteredNetworkCapabilities(
322             NetworkCapabilities networkCapabilities) {
323         NetworkCapabilities.Builder builder =
324                 NetworkCapabilities.Builder.withoutDefaultCapabilities()
325                         .setLinkDownstreamBandwidthKbps(
326                                 networkCapabilities.getLinkDownstreamBandwidthKbps())
327                         .setLinkUpstreamBandwidthKbps(
328                                 networkCapabilities.getLinkUpstreamBandwidthKbps());
329         if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
330             builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
331         }
332         return builder.build();
333     }
334 
335     /** Initials the installed app list by reading from database. */
initialInstalledApp(RawUserData userData)336     public void initialInstalledApp(RawUserData userData) {
337         Map<String, Long> existingInstallApps = mUserDataDao.getAppInstallMap();
338         userData.installedApps = existingInstallApps.keySet();
339     }
340 
341     /** Updates app installed list if necessary. */
342     @VisibleForTesting
updateInstalledApps(RawUserData userData)343     public void updateInstalledApps(RawUserData userData) {
344         try {
345             Map<String, Long> existingInstallApps = mUserDataDao.getAppInstallMap();
346             PackageManager packageManager = mContext.getPackageManager();
347 
348             List<ApplicationInfo> installAppList =
349                     packageManager.getInstalledApplications(
350                             PackageManager.ApplicationInfoFlags.of(GET_META_DATA));
351             Map<String, Long> currentAppInstall =
352                     updateExistingAppInstall(installAppList, existingInstallApps);
353             userData.installedApps = currentAppInstall.keySet();
354             mUserDataDao.insertAppInstall(currentAppInstall);
355             sLogger.d(TAG + ": Update RawUserData installAppList " + userData.installedApps);
356         } catch (Exception e) {
357             sLogger.w(e, TAG + ": Failed to collect installed app list.");
358         }
359     }
360 
361     @VisibleForTesting
updateExistingAppInstall( List<ApplicationInfo> installAppList, Map<String, Long> existingInstallApps)362     Map<String, Long> updateExistingAppInstall(
363             List<ApplicationInfo> installAppList, Map<String, Long> existingInstallApps) {
364         Map<String, Long> currentAppInstallMap = new HashMap<>();
365         long currentTime = MonotonicClock.getInstance().currentTimeMillis();
366 
367         // Get current install apps and update existing app list.
368         for (ApplicationInfo appInfo : installAppList) {
369             String packageName = appInfo.packageName;
370             currentAppInstallMap.put(packageName, currentTime);
371         }
372 
373         // Iterator the new app install list and remove expired apps over 30 days (ttl).
374         long ttl = FlagsFactory.getFlags().getAppInstallHistoryTtlInMillis();
375         for (Map.Entry<String, Long> entry : existingInstallApps.entrySet()) {
376             String packageName = entry.getKey();
377             if (currentAppInstallMap.containsKey(packageName)) continue;
378 
379             long lastUpdateTime = entry.getValue();
380             if (lastUpdateTime >= currentTime - ttl) {
381                 currentAppInstallMap.put(packageName, lastUpdateTime);
382             }
383         }
384         return currentAppInstallMap;
385     }
386 }
387