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