• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.MATCH_UNINSTALLED_PACKAGES;
20 
21 import android.app.usage.UsageStats;
22 import android.app.usage.UsageStatsManager;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.content.res.Configuration;
29 import android.database.Cursor;
30 import android.location.Location;
31 import android.location.LocationManager;
32 import android.net.ConnectivityManager;
33 import android.net.NetworkCapabilities;
34 import android.os.BatteryManager;
35 import android.os.Build;
36 import android.os.Environment;
37 import android.os.StatFs;
38 import android.telephony.TelephonyManager;
39 import android.util.DisplayMetrics;
40 import android.util.Log;
41 import android.view.WindowManager;
42 
43 import androidx.annotation.NonNull;
44 
45 import com.android.internal.annotations.VisibleForTesting;
46 import com.android.ondevicepersonalization.services.data.user.LocationInfo.LocationProvider;
47 
48 import com.google.common.base.Strings;
49 
50 import java.util.ArrayDeque;
51 import java.util.ArrayList;
52 import java.util.Calendar;
53 import java.util.Deque;
54 import java.util.HashMap;
55 import java.util.Iterator;
56 import java.util.List;
57 import java.util.Locale;
58 import java.util.TimeZone;
59 
60 /**
61  * A collector for getting user data signals.
62  * This class only exposes two public operations: periodic update, and
63  * real-time update.
64  * Periodic update operation will be run every 4 hours in the background,
65  * given several on-device resource constraints are satisfied.
66  * Real-time update operation will be run before any ads serving request
67  * and update a few time-sensitive signals in UserData to the latest version.
68  */
69 public class UserDataCollector {
70     public static final int BYTES_IN_MB = 1048576;
71 
72     private static UserDataCollector sUserDataCollector = null;
73     private static final String TAG = "UserDataCollector";
74 
75     @NonNull private final Context mContext;
76     @NonNull private Locale mLocale;
77     @NonNull private final TelephonyManager mTelephonyManager;
78     @NonNull private final NetworkCapabilities mNetworkCapabilities;
79     @NonNull private final LocationManager mLocationManager;
80     @NonNull private final UserDataDao mUserDataDao;
81     // Metadata to keep track of the latest ending timestamp of app usage collection.
82     @NonNull private long mLastTimeMillisAppUsageCollected;
83     // Metadata to track the expired app usage entries, which are to be evicted.
84     @NonNull private Deque<AppUsageEntry> mAllowedAppUsageEntries;
85     // Metadata to track the expired location entries, which are to be evicted.
86     @NonNull private Deque<LocationInfo> mAllowedLocationEntries;
87     // Metadata to track whether UserData has been initialized.
88     @NonNull private boolean mInitialized;
89 
UserDataCollector(Context context, UserDataDao userDataDao)90     private UserDataCollector(Context context, UserDataDao userDataDao) {
91         mContext = context;
92 
93         mLocale = Locale.getDefault();
94         mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
95         ConnectivityManager connectivityManager = mContext.getSystemService(
96                 ConnectivityManager.class);
97         mNetworkCapabilities = connectivityManager.getNetworkCapabilities(
98                 connectivityManager.getActiveNetwork());
99         mLocationManager = mContext.getSystemService(LocationManager.class);
100         mUserDataDao = userDataDao;
101         mLastTimeMillisAppUsageCollected = 0L;
102         mAllowedAppUsageEntries = new ArrayDeque<>();
103         mAllowedLocationEntries = new ArrayDeque<>();
104         mInitialized = false;
105     }
106 
107     /** Returns an instance of UserDataCollector. */
getInstance(Context context)108     public static UserDataCollector getInstance(Context context) {
109         synchronized (UserDataCollector.class) {
110             if (sUserDataCollector == null) {
111                 sUserDataCollector = new UserDataCollector(
112                     context, UserDataDao.getInstance(context));
113             }
114             return sUserDataCollector;
115         }
116     }
117 
118     /**
119      * Returns an instance of the UserDataCollector given a context. This is used
120      * for testing only.
121     */
122     @VisibleForTesting
getInstanceForTest(Context context)123     public static UserDataCollector getInstanceForTest(Context context) {
124         synchronized (UserDataCollector.class) {
125             if (sUserDataCollector == null) {
126                 sUserDataCollector = new UserDataCollector(context,
127                         UserDataDao.getInstanceForTest(context));
128             }
129             return sUserDataCollector;
130         }
131     }
132 
133     /** Update real-time user data to the latest per request. */
getRealTimeData(@onNull RawUserData userData)134     public void getRealTimeData(@NonNull RawUserData userData) {
135         /**
136          * Ads serving requires real-time latency. If user data has not been initialized,
137          * we will skip user data collection for the incoming request and wait until the first
138          * {@link UserDataCollectionJobService} to be scheduled.
139          */
140         if (!mInitialized) {
141             return;
142         }
143         userData.timeMillis = getTimeMillis();
144         userData.utcOffset = getUtcOffset();
145         userData.orientation = getOrientation();
146     }
147 
148     /** Update user data per periodic job servce. */
updateUserData(@onNull RawUserData userData)149     public void updateUserData(@NonNull RawUserData userData) {
150         if (!mInitialized) {
151             initializeUserData(userData);
152             return;
153         }
154         userData.availableBytesMB = getAvailableBytesMB();
155         userData.batteryPct = getBatteryPct();
156         userData.country = getCountry();
157         userData.language = getLanguage();
158         userData.carrier = getCarrier();
159         userData.connectionType = getConnectionType();
160         userData.networkMeteredStatus = getNetworkMeteredStatus();
161         userData.connectionSpeedKbps = getConnectionSpeedKbps();
162 
163         getOSVersions(userData.osVersions);
164         getInstalledApps(userData.appsInfo);
165         getAppUsageStats(userData.appUsageHistory);
166         getLastknownLocation(userData.locationHistory, userData.currentLocation);
167         getCurrentLocation(userData.locationHistory, userData.currentLocation);
168     }
169 
170     /**
171      * Collects in-memory user data signals and stores in a UserData object
172      * for the schedule of {@link UserDataCollectionJobService}
173     */
initializeUserData(@onNull RawUserData userData)174     private void initializeUserData(@NonNull RawUserData userData) {
175         userData.timeMillis = getTimeMillis();
176         userData.utcOffset = getUtcOffset();
177         userData.orientation = getOrientation();
178         userData.availableBytesMB = getAvailableBytesMB();
179         userData.batteryPct = getBatteryPct();
180         userData.country = getCountry();
181         userData.language = getLanguage();
182         userData.carrier = getCarrier();
183         userData.connectionType = getConnectionType();
184         userData.networkMeteredStatus = getNetworkMeteredStatus();
185         userData.connectionSpeedKbps = getConnectionSpeedKbps();
186 
187         getOSVersions(userData.osVersions);
188 
189         getDeviceMetrics(userData.deviceMetrics);
190 
191         getInstalledApps(userData.appsInfo);
192 
193         recoverAppUsageHistogram(userData.appUsageHistory);
194 
195         getAppUsageStats(userData.appUsageHistory);
196         // TODO (b/261748573): add non-trivial tests for location collection and histogram updates.
197         recoverLocationHistogram(userData.locationHistory);
198 
199         getLastknownLocation(userData.locationHistory, userData.currentLocation);
200 
201         getCurrentLocation(userData.locationHistory, userData.currentLocation);
202         mInitialized = true;
203     }
204 
205     /** Collects current system clock on the device. */
206     @VisibleForTesting
getTimeMillis()207     public long getTimeMillis() {
208         return System.currentTimeMillis();
209     }
210 
211     /** Collects current device's time zone in +/- of minutes from UTC. */
212     @VisibleForTesting
getUtcOffset()213     public int getUtcOffset() {
214         return TimeZone.getDefault().getOffset(System.currentTimeMillis()) / 60000;
215     }
216 
217     /** Collects the current device orientation. */
218     @VisibleForTesting
getOrientation()219     public int getOrientation() {
220         return mContext.getResources().getConfiguration().orientation;
221     }
222 
223     /** Collects available bytes and converts to MB. */
224     @VisibleForTesting
getAvailableBytesMB()225     public int getAvailableBytesMB() {
226         StatFs statFs = new StatFs(Environment.getDataDirectory().getPath());
227         return (int) (statFs.getAvailableBytes() / BYTES_IN_MB);
228     }
229 
230     /** Collects the battery percentage of the device. */
231     @VisibleForTesting
getBatteryPct()232     public int getBatteryPct() {
233         IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
234         Intent batteryStatus = mContext.registerReceiver(null, ifilter);
235 
236         int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
237         int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
238         if (level >= 0 && scale > 0) {
239             return Math.round(level * 100.0f / (float) scale);
240         }
241         return 0;
242     }
243 
244     /** Collects current device's country information. */
245     @VisibleForTesting
getCountry()246     public Country getCountry() {
247         String countryCode = mLocale.getISO3Country();
248         if (Strings.isNullOrEmpty(countryCode)) {
249             return Country.UNKNOWN;
250         } else {
251             Country country = Country.UNKNOWN;
252             try {
253                 country = Country.valueOf(countryCode);
254             } catch (IllegalArgumentException iae) {
255                 Log.e(TAG, "Country code cannot match to a country.", iae);
256                 return country;
257             }
258             return country;
259         }
260     }
261 
262     /** Collects current device's language information. */
263     @VisibleForTesting
getLanguage()264     public Language getLanguage() {
265         String langCode = mLocale.getLanguage();
266         if (Strings.isNullOrEmpty(langCode)) {
267             return Language.UNKNOWN;
268         } else {
269             Language language = Language.UNKNOWN;
270             try {
271                 language = Language.valueOf(langCode.toUpperCase(Locale.US));
272             } catch (IllegalArgumentException iae) {
273                 Log.e(TAG, "Language code cannot match to a language.", iae);
274                 return language;
275             }
276             return language;
277         }
278     }
279 
280     /** Collects carrier info. */
281     @VisibleForTesting
getCarrier()282     public Carrier getCarrier() {
283         // TODO: handle i18n later if the carrier's name is in non-English script.
284         switch (mTelephonyManager.getSimOperatorName().toUpperCase(Locale.US)) {
285             case "RELIANCE JIO":
286                 return Carrier.RELIANCE_JIO;
287             case "VODAFONE":
288                 return Carrier.VODAFONE;
289             case "T-MOBILE - US":
290             case "T-MOBILE":
291                 return Carrier.T_MOBILE;
292             case "VERIZON WIRELESS":
293                 return Carrier.VERIZON_WIRELESS;
294             case "AIRTEL":
295                 return Carrier.AIRTEL;
296             case "ORANGE":
297                 return Carrier.ORANGE;
298             case "NTT DOCOMO":
299                 return Carrier.NTT_DOCOMO;
300             case "MOVISTAR":
301                 return Carrier.MOVISTAR;
302             case "AT&T":
303                 return Carrier.AT_T;
304             case "TELCEL":
305                 return Carrier.TELCEL;
306             case "VIVO":
307                 return Carrier.VIVO;
308             case "VI":
309                 return Carrier.VI;
310             case "TIM":
311                 return Carrier.TIM;
312             case "O2":
313                 return Carrier.O2;
314             case "TELEKOM":
315                 return Carrier.TELEKOM;
316             case "CLARO BR":
317                 return Carrier.CLARO_BR;
318             case "SK TELECOM":
319                 return Carrier.SK_TELECOM;
320             case "MTC":
321                 return Carrier.MTC;
322             case "AU":
323                 return Carrier.AU;
324             case "TELE2":
325                 return Carrier.TELE2;
326             case "SFR":
327                 return Carrier.SFR;
328             case "ETECSA":
329                 return Carrier.ETECSA;
330             case "IR-MCI (HAMRAHE AVVAL)":
331                 return Carrier.IR_MCI;
332             case "KT":
333                 return Carrier.KT;
334             case "TELKOMSEL":
335                 return Carrier.TELKOMSEL;
336             case "IRANCELL":
337                 return Carrier.IRANCELL;
338             case "MEGAFON":
339                 return Carrier.MEGAFON;
340             case "TELEFONICA":
341                 return Carrier.TELEFONICA;
342             default:
343                 return Carrier.UNKNOWN;
344         }
345     }
346 
347     /**
348      * Collects device OS version info.
349      * ODA only identifies three valid raw forms of OS releases
350      * and convert it to the three-version format.
351      * 13 -> 13.0.0
352      * 8.1 -> 8.1.0
353      * 4.1.2 as it is.
354      */
355     @VisibleForTesting
getOSVersions(@onNull OSVersion osVersions)356     public void getOSVersions(@NonNull OSVersion osVersions) {
357         String osRelease = Build.VERSION.RELEASE;
358         int major = 0;
359         int minor = 0;
360         int micro = 0;
361         try {
362             major = Integer.parseInt(osRelease);
363         } catch (NumberFormatException nfe1) {
364             try {
365                 String[] versions = osRelease.split("[.]");
366                 if (versions.length == 2) {
367                     major = Integer.parseInt(versions[0]);
368                     minor = Integer.parseInt(versions[1]);
369                 } else if (versions.length == 3) {
370                     major = Integer.parseInt(versions[0]);
371                     minor = Integer.parseInt(versions[1]);
372                     micro = Integer.parseInt(versions[2]);
373                 } else {
374                     // An irregular release like "UpsideDownCake"
375                     Log.e(TAG, "OS release string cannot be matched to a regular version.", nfe1);
376                 }
377             } catch (NumberFormatException nfe2) {
378                 // An irrgular release like "QKQ1.200830.002"
379                 Log.e(TAG, "OS release string cannot be matched to a regular version.", nfe2);
380             }
381         } finally {
382             osVersions.major = major;
383             osVersions.minor = minor;
384             osVersions.micro = micro;
385         }
386     }
387 
388     /** Collects connection type. */
389     @VisibleForTesting
getConnectionType()390     public RawUserData.ConnectionType getConnectionType() {
391         if (mNetworkCapabilities == null) {
392             return RawUserData.ConnectionType.UNKNOWN;
393         } else if (mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
394             switch (mTelephonyManager.getDataNetworkType()) {
395                 case TelephonyManager.NETWORK_TYPE_1xRTT:
396                 case TelephonyManager.NETWORK_TYPE_CDMA:
397                 case TelephonyManager.NETWORK_TYPE_EDGE:
398                 case TelephonyManager.NETWORK_TYPE_GPRS:
399                 case TelephonyManager.NETWORK_TYPE_GSM:
400                 case TelephonyManager.NETWORK_TYPE_IDEN:
401                     return RawUserData.ConnectionType.CELLULAR_2G;
402                 case TelephonyManager.NETWORK_TYPE_EHRPD:
403                 case TelephonyManager.NETWORK_TYPE_EVDO_0:
404                 case TelephonyManager.NETWORK_TYPE_EVDO_A:
405                 case TelephonyManager.NETWORK_TYPE_EVDO_B:
406                 case TelephonyManager.NETWORK_TYPE_HSDPA:
407                 case TelephonyManager.NETWORK_TYPE_HSPA:
408                 case TelephonyManager.NETWORK_TYPE_HSPAP:
409                 case TelephonyManager.NETWORK_TYPE_HSUPA:
410                 case TelephonyManager.NETWORK_TYPE_TD_SCDMA:
411                 case TelephonyManager.NETWORK_TYPE_UMTS:
412                     return RawUserData.ConnectionType.CELLULAR_3G;
413                 case TelephonyManager.NETWORK_TYPE_LTE:
414                 case TelephonyManager.NETWORK_TYPE_IWLAN:
415                     return RawUserData.ConnectionType.CELLULAR_4G;
416                 case TelephonyManager.NETWORK_TYPE_NR:
417                     return RawUserData.ConnectionType.CELLULAR_5G;
418                 default:
419                     return RawUserData.ConnectionType.UNKNOWN;
420             }
421         } else if (mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
422             return RawUserData.ConnectionType.WIFI;
423         } else if (mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
424             return RawUserData.ConnectionType.ETHERNET;
425         }
426         return RawUserData.ConnectionType.UNKNOWN;
427     }
428 
429     /** Collects metered status. */
430     @VisibleForTesting
getNetworkMeteredStatus()431     public boolean getNetworkMeteredStatus() {
432         if (mNetworkCapabilities == null) {
433             return false;
434         }
435         int[] capabilities = mNetworkCapabilities.getCapabilities();
436         for (int i = 0; i < capabilities.length; ++i) {
437             if (capabilities[i] == NetworkCapabilities.NET_CAPABILITY_NOT_METERED) {
438                 return false;
439             }
440         }
441         return true;
442     }
443 
444     /** Collects connection speed in kbps */
445     @VisibleForTesting
getConnectionSpeedKbps()446     public int getConnectionSpeedKbps() {
447         if (mNetworkCapabilities == null) {
448             return 0;
449         }
450         return mNetworkCapabilities.getLinkDownstreamBandwidthKbps();
451     }
452 
453     /** Collects current device's static metrics. */
454     @VisibleForTesting
getDeviceMetrics(DeviceMetrics deviceMetrics)455     public void getDeviceMetrics(DeviceMetrics deviceMetrics) {
456         if (deviceMetrics == null) {
457             return;
458         }
459         deviceMetrics.make = getDeviceMake();
460         deviceMetrics.model = getDeviceModel();
461         deviceMetrics.screenHeight = mContext.getResources().getConfiguration().screenHeightDp;
462         deviceMetrics.screenWidth = mContext.getResources().getConfiguration().screenWidthDp;
463         DisplayMetrics displayMetrics = new DisplayMetrics();
464         WindowManager wm = mContext.getSystemService(WindowManager.class);
465         wm.getDefaultDisplay().getMetrics(displayMetrics);
466         deviceMetrics.xdpi = displayMetrics.xdpi;
467         deviceMetrics.ydpi = displayMetrics.ydpi;
468         deviceMetrics.pxRatio = displayMetrics.density;
469     }
470 
471     /**
472      * Collects device make info.
473      */
474     @VisibleForTesting
getDeviceMake()475     public Make getDeviceMake() {
476         String manufacturer = Build.MANUFACTURER.toUpperCase(Locale.US);
477         Make make = Make.UNKNOWN;
478         try {
479             make = Make.valueOf(manufacturer);
480         } catch (IllegalArgumentException iae) {
481             // handle corner cases for irregularly formatted string.
482             make = getMakeFromSpecialString(manufacturer);
483             if (make == Make.UNKNOWN) {
484                 Log.e(TAG, "Manufacturer string cannot match to an available make type.", iae);
485             }
486             return make;
487         }
488         return make;
489     }
490 
491     /** Collects device model info */
492     @VisibleForTesting
getDeviceModel()493     public Model getDeviceModel() {
494         // Uppercase and replace whitespace/hyphen with underscore character
495         String deviceModel = Build.MODEL.toUpperCase(Locale.US).replace(' ', '_').replace('-', '_');
496         Model model = Model.UNKNOWN;
497         try {
498             model = Model.valueOf(deviceModel);
499         } catch (IllegalArgumentException iae) {
500             // handle corner cases for irregularly formatted string.
501             model = getModelFromSpecialString(deviceModel);
502             if (model == Model.UNKNOWN) {
503                 Log.e(TAG, "Model string cannot match to an available make type.", iae);
504             }
505             return model;
506         }
507         return model;
508     }
509 
510     /**
511      * Helper function that handles irregularly formatted manufacturer string,
512      * which cannot be directly cast into enums.
513      */
getMakeFromSpecialString(String deviceMake)514     private Make getMakeFromSpecialString(String deviceMake) {
515         switch (deviceMake) {
516             case "TECNO MOBILE LIMITED":
517                 return Make.TECNO;
518             case "INFINIX MOBILITY LIMITED":
519                 return Make.INFINIX;
520             case "HMD GLOBAL":
521                 return Make.HMD_GLOBAL;
522             case "LGE":
523             case "LG ELECTRONICS":
524                 return Make.LG_ELECTRONICS;
525             case "SKYWORTHDIGITAL":
526                 return Make.SKYWORTH;
527             case "ITEL":
528             case "ITEL MOBILE LIMITED":
529                 return Make.ITEL_MOBILE;
530             case "KAON":
531             case "KAONMEDIA":
532                 return Make.KAON_MEDIA;
533             case "ZEBRA TECHNOLOGIES":
534                 return Make.ZEBRA_TECHNOLOGIES;
535             case "VOLVOCARS":
536                 return Make.VOLVO_CARS;
537             case "SUMITOMOELECTRICINDUSTRIES":
538                 return Make.SUMITOMO_ELECTRIC_INDUSTRIES;
539             case "STARHUB":
540                 return Make.STAR_HUB;
541             case "GENERALMOBILE":
542                 return Make.GENERAL_MOBILE;
543             case "KONNECTONE":
544                 return Make.KONNECT_ONE;
545             case "CASIO COMPUTER CO., LTD.":
546                 return Make.CASIO_COMPUTER;
547             case "SEI":
548             case "SEI ROBOTICS":
549                 return Make.SEI_ROBOTICS;
550             case "EASTAEON":
551                 return Make.EAST_AEON;
552             case "HIMEDIA":
553                 return Make.HI_MEDIA;
554             case "HOT PEPPER INC":
555                 return Make.HOT_PEPPER;
556             case "SKY":
557             case "SKY DEVICES":
558             case "SKYDEVICES":
559                 return Make.SKY_DEVICES;
560             case "FOXX DEVELOPMENT INC.":
561                 return Make.FOXX_DEVELOPMENT;
562             case "RELIANCE COMMUNICATIONS":
563                 return Make.RELIANCE_COMMUNICATIONS;
564             case "EMPORIA TELECOM GMBH & CO. KG":
565                 return Make.EMPORIA;
566             case "CIPHERLAB":
567                 return Make.CIPHER_LAB;
568             case "ISAFEMOBILE":
569                 return Make.ISAFE_MOBILE;
570             case "CLARO COLUMBIA":
571                 return Make.CLARO;
572             case "MYPHONE":
573                 return Make.MY_PHONE;
574             case "TAG HEUER":
575                 return Make.TAG_HEUER;
576             case "XWIRELESSLLC":
577                 return Make.XWIRELESS;
578             default:
579                 return Make.UNKNOWN;
580         }
581     }
582 
583     /**
584      * Helper function that handles irregularly formatted model string,
585      * which cannot be directly cast into enums.
586      */
getModelFromSpecialString(String deviceModel)587     private Model getModelFromSpecialString(String deviceModel) {
588         switch (deviceModel) {
589             case "AT&T_TV":
590                 return Model.ATT_TV;
591             case "XVIEW+":
592                 return Model.XVIEW_PLUS;
593             case "2201117TG":
594                 return Model.REDMI_2201117TG;
595             case "2201117TY":
596                 return Model.REDMI_2201117TY;
597             case "B860H_V5.0":
598                 return Model.B860H_V5;
599             case "MOTO_G(20)":
600                 return Model.MOTO_G20;
601             case "2020/2021_UHD_ANDROID_TV":
602                 return Model.PHILIPS_2020_2021_UHD_ANDROID_TV;
603             case "2109119DG":
604                 return Model.XIAOMI_2109119DG;
605             case "MOTO_G(9)_PLAY":
606                 return Model.MOTO_G9_PLAY;
607             case "MOTO_E(7)":
608                 return Model.MOTO_E7;
609             case "2021/22_PHILIPS_UHD_ANDROID_TV":
610                 return Model.PHILIPS_2021_22_UHD_ANDROID_TV;
611             case "MOTO_G(30)":
612                 return Model.MOTO_G30;
613             case "MOTO_G_POWER_(2021)":
614                 return Model.MOTO_G_POWER_2021;
615             case "MOTO_G(7)_POWER":
616                 return Model.MOTO_G7_POWER;
617             case "OTT_XVIEW+_AV1":
618                 return Model.OTT_XVIEW_PLUS_AV1;
619             default:
620                 return Model.UNKNOWN;
621         }
622     }
623 
624     /** Get app install and uninstall record. */
625     @VisibleForTesting
getInstalledApps(@onNull List<AppInfo> appsInfo)626     public void getInstalledApps(@NonNull List<AppInfo> appsInfo) {
627         appsInfo.clear();
628         PackageManager packageManager = mContext.getPackageManager();
629         for (ApplicationInfo appInfo :
630                 packageManager.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES)) {
631             AppInfo app = new AppInfo();
632             app.packageName = appInfo.packageName;
633             if ((appInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0) {
634                 app.installed = true;
635             } else {
636                 app.installed = false;
637             }
638             appsInfo.add(app);
639         }
640     }
641 
642     /**
643      * Get 24-hour app usage stats from [yesterday's midnight] to [tonight's midnight],
644      * write them to database, and update the [appUsageHistory] histogram.
645      * Skip the current collection cycle if yesterday's stats has been collected.
646     */
647     @VisibleForTesting
getAppUsageStats(HashMap<String, Long> appUsageHistory)648     public void getAppUsageStats(HashMap<String, Long> appUsageHistory) {
649         Calendar cal = Calendar.getInstance();
650         // Obtain the 24-hour query range between [yesterday midnight] and [today midnight].
651         cal.set(Calendar.MILLISECOND, 0);
652         cal.set(Calendar.SECOND, 0);
653         cal.set(Calendar.MINUTE, 0);
654         cal.set(Calendar.HOUR_OF_DAY, 0);
655         final long endTimeMillis = cal.getTimeInMillis();
656 
657         // Skip the current collection cycle.
658         if (endTimeMillis == mLastTimeMillisAppUsageCollected) {
659             return;
660         }
661 
662         // Collect yesterday's app usage stats.
663         cal.add(Calendar.DATE, -1);
664         final long startTimeMillis = cal.getTimeInMillis();
665         UsageStatsManager usageStatsManager = mContext.getSystemService(UsageStatsManager.class);
666         final List<UsageStats> statsList = usageStatsManager.queryUsageStats(
667                 UsageStatsManager.INTERVAL_BEST, startTimeMillis, endTimeMillis);
668 
669         List<AppUsageEntry> appUsageEntries = new ArrayList<>();
670         for (UsageStats stats: statsList) {
671             if (stats.getTotalTimeVisible() == 0) {
672                 continue;
673             }
674             appUsageEntries.add(new AppUsageEntry(stats.getPackageName(),
675                     startTimeMillis, endTimeMillis, stats.getTotalTimeVisible()));
676         }
677 
678         // TODO(267678607): refactor the business logic when no stats is available.
679         if (appUsageEntries.size() == 0) {
680             return;
681         }
682 
683         // Update database.
684         if (!mUserDataDao.batchInsertAppUsageStatsData(appUsageEntries)) {
685             return;
686         }
687         // Update in-memory histogram.
688         updateAppUsageHistogram(appUsageHistory, appUsageEntries);
689         // Update metadata if all steps succeed as a transaction.
690         mLastTimeMillisAppUsageCollected = endTimeMillis;
691     }
692 
693     /**
694      * Update histogram and handle TTL deletion for app usage (30 days).
695      */
updateAppUsageHistogram(HashMap<String, Long> appUsageHistory, List<AppUsageEntry> entries)696     private void updateAppUsageHistogram(HashMap<String, Long> appUsageHistory,
697             List<AppUsageEntry> entries) {
698         for (AppUsageEntry entry: entries) {
699             mAllowedAppUsageEntries.add(entry);
700             appUsageHistory.put(entry.packageName, appUsageHistory.getOrDefault(
701                     entry.packageName, 0L) + entry.totalTimeUsedMillis);
702         }
703         // Backtrack 30 days
704         Calendar cal = Calendar.getInstance();
705         cal.add(Calendar.DATE, -1 * UserDataDao.TTL_IN_MEMORY_DAYS);
706         final long thresholdTimeMillis = cal.getTimeInMillis();
707 
708         // TTL deletion algorithm
709         while (!mAllowedAppUsageEntries.isEmpty()
710                 && mAllowedAppUsageEntries.peekFirst().endTimeMillis < thresholdTimeMillis) {
711             AppUsageEntry evictedEntry = mAllowedAppUsageEntries.removeFirst();
712             if (appUsageHistory.containsKey(evictedEntry.packageName)) {
713                 final long updatedTotalTime = appUsageHistory.get(
714                         evictedEntry.packageName) - evictedEntry.totalTimeUsedMillis;
715                 if (updatedTotalTime == 0) {
716                     appUsageHistory.remove(evictedEntry.packageName);
717                 } else {
718                     appUsageHistory.put(evictedEntry.packageName, updatedTotalTime);
719                 }
720             }
721         }
722     }
723 
724     /** Get last known location information. The result is immediate. */
725     @VisibleForTesting
getLastknownLocation(@onNull HashMap<LocationInfo, Long> locationHistory, @NonNull LocationInfo locationInfo)726     public void getLastknownLocation(@NonNull HashMap<LocationInfo, Long> locationHistory,
727             @NonNull LocationInfo locationInfo) {
728         Location location = mLocationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER);
729         if (location != null) {
730             if (!setLocationInfo(location, locationInfo)) {
731                 return;
732             }
733             updateLocationHistogram(locationHistory, locationInfo);
734         }
735     }
736 
737     /** Get current location information. The result takes some time to generate. */
738     @VisibleForTesting
getCurrentLocation(@onNull HashMap<LocationInfo, Long> locationHistory, @NonNull LocationInfo locationInfo)739     public void getCurrentLocation(@NonNull HashMap<LocationInfo, Long> locationHistory,
740             @NonNull LocationInfo locationInfo) {
741         String currentProvider = LocationManager.GPS_PROVIDER;
742         if (mLocationManager.getProvider(currentProvider) == null) {
743             currentProvider = LocationManager.FUSED_PROVIDER;
744         }
745         mLocationManager.getCurrentLocation(
746                 currentProvider,
747                 null,
748                 mContext.getMainExecutor(),
749                 location -> {
750                     if (location != null) {
751                         if (!setLocationInfo(location, locationInfo)) {
752                             return;
753                         }
754                         updateLocationHistogram(locationHistory, locationInfo);
755                     }
756                 }
757         );
758     }
759 
760     /**
761      * Persist collected location info and populate the in-memory current location.
762      * The method should succeed or fail as a transaction to avoid discrepancies between
763      * database and memory.
764      * @return true if location info collection is successful, false otherwise.
765      */
setLocationInfo(Location location, LocationInfo locationInfo)766     private boolean setLocationInfo(Location location, LocationInfo locationInfo) {
767         long timeMillis = getTimeMillis() - location.getElapsedRealtimeAgeMillis();
768         double truncatedLatitude = Math.round(location.getLatitude() *  10000.0) / 10000.0;
769         double truncatedLongitude = Math.round(location.getLongitude() *  10000.0) / 10000.0;
770         LocationInfo.LocationProvider locationProvider = LocationProvider.UNKNOWN;
771         boolean isPrecise = false;
772 
773         String provider = location.getProvider();
774         if (LocationManager.GPS_PROVIDER.equals(provider)) {
775             locationProvider = LocationInfo.LocationProvider.GPS;
776             isPrecise = true;
777         } else {
778             if (LocationManager.NETWORK_PROVIDER.equals(provider)) {
779                 locationProvider = LocationInfo.LocationProvider.NETWORK;
780             }
781         }
782 
783         if (!mUserDataDao.insertLocationHistoryData(timeMillis, Double.toString(truncatedLatitude),
784                 Double.toString(truncatedLongitude), locationProvider.ordinal(), isPrecise)) {
785             return false;
786         }
787         // update user's current location
788         locationInfo.timeMillis = timeMillis;
789         locationInfo.latitude = truncatedLatitude;
790         locationInfo.longitude = truncatedLongitude;
791         locationInfo.provider = locationProvider;
792         locationInfo.isPreciseLocation = isPrecise;
793         return true;
794     }
795 
796     /**
797      * Update histogram and handle TTL deletion for location history (30 days).
798      */
updateLocationHistogram(HashMap<LocationInfo, Long> locationHistory, LocationInfo newLocation)799     private void updateLocationHistogram(HashMap<LocationInfo, Long> locationHistory,
800             LocationInfo newLocation) {
801         LocationInfo curLocation = mAllowedLocationEntries.peekLast();
802         // must be a deep copy
803         mAllowedLocationEntries.add(new LocationInfo(newLocation));
804         if (curLocation != null) {
805             long durationMillis = newLocation.timeMillis - curLocation.timeMillis;
806             locationHistory.put(curLocation,
807                     locationHistory.getOrDefault(curLocation, 0L) + durationMillis);
808         }
809 
810         // Backtrack 30 days
811         Calendar cal = Calendar.getInstance();
812         cal.add(Calendar.DATE, -1 * UserDataDao.TTL_IN_MEMORY_DAYS);
813         final long thresholdTimeMillis = cal.getTimeInMillis();
814 
815         // TTL deletion algorithm for locations
816         while (!mAllowedLocationEntries.isEmpty()
817                 && mAllowedLocationEntries.peekFirst().timeMillis < thresholdTimeMillis) {
818             LocationInfo evictedLocation = mAllowedLocationEntries.removeFirst();
819             if (!mAllowedLocationEntries.isEmpty()) {
820                 long evictedDuration =  mAllowedLocationEntries.peekFirst().timeMillis
821                         - evictedLocation.timeMillis;
822                 if (locationHistory.containsKey(evictedLocation)) {
823                     long updatedDuration = locationHistory.get(evictedLocation) - evictedDuration;
824                     if (updatedDuration == 0) {
825                         locationHistory.remove(evictedLocation);
826                     } else {
827                         locationHistory.put(evictedLocation, updatedDuration);
828                     }
829                 }
830             }
831         }
832     }
833 
834     /**
835      * Setter to update locale for testing purpose.
836      */
837     @VisibleForTesting
setLocale(Locale locale)838     public void setLocale(Locale locale) {
839         mLocale = locale;
840     }
841 
842     /**
843      * Util to reset all fields in [UserData] to default for testing purpose
844      */
clearUserData(@onNull RawUserData userData)845     public void clearUserData(@NonNull RawUserData userData) {
846         userData.timeMillis = 0;
847         userData.utcOffset = 0;
848         userData.orientation = Configuration.ORIENTATION_PORTRAIT;
849         userData.availableBytesMB = 0;
850         userData.batteryPct = 0;
851         userData.country = Country.UNKNOWN;
852         userData.language = Language.UNKNOWN;
853         userData.carrier = Carrier.UNKNOWN;
854         userData.osVersions = new OSVersion();
855         userData.connectionType = RawUserData.ConnectionType.UNKNOWN;
856         userData.networkMeteredStatus = false;
857         userData.connectionSpeedKbps = 0;
858         userData.deviceMetrics = new DeviceMetrics();
859         userData.appsInfo.clear();
860         userData.appUsageHistory.clear();
861         userData.locationHistory.clear();
862     }
863 
864     /**
865      * Util to reset all in-memory metadata for testing purpose.
866      */
clearMetadata()867     public void clearMetadata() {
868         mInitialized = false;
869         mLastTimeMillisAppUsageCollected = 0L;
870         mAllowedAppUsageEntries = new ArrayDeque<>();
871         mAllowedLocationEntries = new ArrayDeque<>();
872     }
873 
874     /**
875      * Reset app usage histogram and metadata in case of system crash.
876      * Only used during initial data collection.
877      */
878     @VisibleForTesting
recoverAppUsageHistogram(HashMap<String, Long> appUsageHistory)879     public void recoverAppUsageHistogram(HashMap<String, Long> appUsageHistory) {
880         Cursor cursor = mUserDataDao.readAppUsageInLastXDays(UserDataDao.TTL_IN_MEMORY_DAYS);
881         if (cursor == null) {
882             return;
883         }
884         // Metadata to be reset.
885         appUsageHistory.clear();
886         mLastTimeMillisAppUsageCollected = 0L;
887         mAllowedAppUsageEntries.clear();
888 
889         if (cursor.moveToFirst()) {
890             while (!cursor.isAfterLast()) {
891                 String packageName = cursor.getString(cursor.getColumnIndex(
892                         UserDataTables.AppUsageHistory.PACKAGE_NAME));
893                 long startTimeMillis = cursor.getLong(cursor.getColumnIndex(
894                         UserDataTables.AppUsageHistory.STARTING_TIME_SEC));
895                 long endTimeMillis = cursor.getLong(cursor.getColumnIndex(
896                         UserDataTables.AppUsageHistory.ENDING_TIME_SEC));
897                 long totalTimeUsedMillis =  cursor.getLong(cursor.getColumnIndex(
898                         UserDataTables.AppUsageHistory.TOTAL_TIME_USED_SEC));
899                 mAllowedAppUsageEntries.add(new AppUsageEntry(packageName,
900                         startTimeMillis, endTimeMillis, totalTimeUsedMillis));
901                 appUsageHistory.put(packageName, appUsageHistory.getOrDefault(packageName,
902                         0L) + totalTimeUsedMillis);
903                 cursor.moveToNext();
904             }
905         }
906 
907         if (cursor.moveToLast()) {
908             mLastTimeMillisAppUsageCollected = cursor.getLong(cursor.getColumnIndex(
909                     UserDataTables.AppUsageHistory.ENDING_TIME_SEC));
910         }
911     }
912 
913     /**
914      * Reset location histogram and metadata in case of system crash.
915      */
916     @VisibleForTesting
recoverLocationHistogram(HashMap<LocationInfo, Long> locationHistory)917     public void recoverLocationHistogram(HashMap<LocationInfo, Long> locationHistory) {
918         Cursor cursor = mUserDataDao.readLocationInLastXDays(mUserDataDao.TTL_IN_MEMORY_DAYS);
919         if (cursor == null) {
920             return;
921         }
922         // Metadata to be reset.
923         locationHistory.clear();
924         mAllowedLocationEntries.clear();
925 
926         if (cursor.moveToFirst()) {
927             while (!cursor.isAfterLast()) {
928                 long timeMillis = cursor.getLong(cursor.getColumnIndex(
929                         UserDataTables.LocationHistory.TIME_SEC));
930                 String latitude = cursor.getString(cursor.getColumnIndex(
931                         UserDataTables.LocationHistory.LATITUDE));
932                 String longitude = cursor.getString(cursor.getColumnIndex(
933                         UserDataTables.LocationHistory.LONGITUDE));
934                 int source = cursor.getInt(cursor.getColumnIndex(
935                         UserDataTables.LocationHistory.SOURCE));
936                 boolean isPrecise = cursor.getInt(cursor.getColumnIndex(
937                         UserDataTables.LocationHistory.IS_PRECISE)) > 0;
938                 mAllowedLocationEntries.add(new LocationInfo(timeMillis,
939                         Double.parseDouble(latitude),
940                         Double.parseDouble(longitude),
941                         LocationProvider.fromInteger(source),
942                         isPrecise));
943                 cursor.moveToNext();
944             }
945         }
946 
947         Iterator<LocationInfo> iterator = mAllowedLocationEntries.iterator();
948         while (iterator.hasNext()) {
949             LocationInfo currentLocation = iterator.next();
950             if (!iterator.hasNext()) {
951                 return;
952             }
953             LocationInfo nextLocation = iterator.next();
954             final long duration = nextLocation.timeMillis - currentLocation.timeMillis;
955             if (duration < 0) {
956                 Log.v(TAG, "LocationInfo entries are retrieved with wrong order.");
957             }
958             locationHistory.put(currentLocation,
959                     locationHistory.getOrDefault(currentLocation, 0L) + duration);
960         }
961     }
962 
963     @VisibleForTesting
isInitialized()964     public boolean isInitialized() {
965         return mInitialized;
966     }
967 
968     @VisibleForTesting
getLastTimeMillisAppUsageCollected()969     public long getLastTimeMillisAppUsageCollected() {
970         return mLastTimeMillisAppUsageCollected;
971     }
972 
973     @VisibleForTesting
getAllowedAppUsageEntries()974     public Deque<AppUsageEntry> getAllowedAppUsageEntries() {
975         return mAllowedAppUsageEntries;
976     }
977 
978     @VisibleForTesting
getAllowedLocationEntries()979     public Deque<LocationInfo> getAllowedLocationEntries() {
980         return mAllowedLocationEntries;
981     }
982 
983     /**
984      * Clear all user data in database for testing purpose.
985      */
clearDatabase()986     public void clearDatabase() {
987         mUserDataDao.clearUserData();
988     }
989 }
990