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