1 /** 2 * Copyright (C) 2020 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.server.profcollect; 18 19 import android.app.job.JobInfo; 20 import android.app.job.JobParameters; 21 import android.app.job.JobScheduler; 22 import android.app.job.JobService; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.ResolveInfo; 27 import android.os.Handler; 28 import android.os.IBinder.DeathRecipient; 29 import android.os.Looper; 30 import android.os.RemoteException; 31 import android.os.ServiceManager; 32 import android.os.SystemProperties; 33 import android.os.UpdateEngine; 34 import android.os.UpdateEngineCallback; 35 import android.os.UserHandle; 36 import android.os.UserManager; 37 import android.provider.DeviceConfig; 38 import android.util.Log; 39 40 import com.android.internal.R; 41 import com.android.server.IoThread; 42 import com.android.server.LocalServices; 43 import com.android.server.SystemService; 44 import com.android.server.wm.ActivityMetricsLaunchObserver; 45 import com.android.server.wm.ActivityMetricsLaunchObserverRegistry; 46 import com.android.server.wm.ActivityTaskManagerInternal; 47 48 import java.nio.file.Files; 49 import java.nio.file.Paths; 50 import java.util.List; 51 import java.util.concurrent.ThreadLocalRandom; 52 import java.util.concurrent.TimeUnit; 53 54 /** 55 * System-server-local proxy into the {@code IProfcollectd} native service. 56 */ 57 public final class ProfcollectForwardingService extends SystemService { 58 public static final String LOG_TAG = "ProfcollectForwardingService"; 59 60 private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); 61 62 private static final long BG_PROCESS_PERIOD = TimeUnit.DAYS.toMillis(1); // every 1 day. 63 64 private IProfCollectd mIProfcollect; 65 private static ProfcollectForwardingService sSelfService; 66 private final Handler mHandler = new ProfcollectdHandler(IoThread.getHandler().getLooper()); 67 ProfcollectForwardingService(Context context)68 public ProfcollectForwardingService(Context context) { 69 super(context); 70 71 if (sSelfService != null) { 72 throw new AssertionError("only one service instance allowed"); 73 } 74 sSelfService = this; 75 } 76 77 /** 78 * Check whether profcollect is enabled through device config. 79 */ enabled()80 public static boolean enabled() { 81 return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT, "enabled", 82 false) || SystemProperties.getBoolean("persist.profcollectd.enabled_override", false); 83 } 84 85 @Override onStart()86 public void onStart() { 87 if (DEBUG) { 88 Log.d(LOG_TAG, "Profcollect forwarding service start"); 89 } 90 connectNativeService(); 91 } 92 93 @Override onBootPhase(int phase)94 public void onBootPhase(int phase) { 95 if (phase == PHASE_BOOT_COMPLETED) { 96 if (mIProfcollect == null) { 97 return; 98 } 99 if (serviceHasSupportedTraceProvider()) { 100 registerObservers(); 101 } 102 ProfcollectBGJobService.schedule(getContext()); 103 } 104 } 105 serviceHasSupportedTraceProvider()106 private boolean serviceHasSupportedTraceProvider() { 107 if (mIProfcollect == null) { 108 return false; 109 } 110 try { 111 return !mIProfcollect.get_supported_provider().isEmpty(); 112 } catch (RemoteException e) { 113 Log.e(LOG_TAG, e.getMessage()); 114 return false; 115 } 116 } 117 tryConnectNativeService()118 private boolean tryConnectNativeService() { 119 if (connectNativeService()) { 120 return true; 121 } 122 // Cannot connect to the native service at this time, retry after a short delay. 123 mHandler.sendEmptyMessageDelayed(ProfcollectdHandler.MESSAGE_BINDER_CONNECT, 5000); 124 return false; 125 } 126 connectNativeService()127 private boolean connectNativeService() { 128 try { 129 IProfCollectd profcollectd = 130 IProfCollectd.Stub.asInterface( 131 ServiceManager.getServiceOrThrow("profcollectd")); 132 profcollectd.asBinder().linkToDeath(new ProfcollectdDeathRecipient(), /*flags*/0); 133 mIProfcollect = profcollectd; 134 return true; 135 } catch (ServiceManager.ServiceNotFoundException | RemoteException e) { 136 Log.w(LOG_TAG, "Failed to connect profcollectd binder service."); 137 return false; 138 } 139 } 140 141 private class ProfcollectdHandler extends Handler { ProfcollectdHandler(Looper looper)142 public ProfcollectdHandler(Looper looper) { 143 super(looper); 144 } 145 146 public static final int MESSAGE_BINDER_CONNECT = 0; 147 148 @Override handleMessage(android.os.Message message)149 public void handleMessage(android.os.Message message) { 150 switch (message.what) { 151 case MESSAGE_BINDER_CONNECT: 152 connectNativeService(); 153 break; 154 default: 155 throw new AssertionError("Unknown message: " + message.toString()); 156 } 157 } 158 } 159 160 private class ProfcollectdDeathRecipient implements DeathRecipient { 161 @Override binderDied()162 public void binderDied() { 163 Log.w(LOG_TAG, "profcollectd has died"); 164 165 mIProfcollect = null; 166 tryConnectNativeService(); 167 } 168 } 169 170 /** 171 * Background trace process service. 172 */ 173 public static class ProfcollectBGJobService extends JobService { 174 // Unique ID in system service 175 private static final int JOB_IDLE_PROCESS = 260817; 176 private static final ComponentName JOB_SERVICE_NAME = new ComponentName( 177 "android", 178 ProfcollectBGJobService.class.getName()); 179 180 /** 181 * Attach the service to the system job scheduler. 182 */ schedule(Context context)183 public static void schedule(Context context) { 184 JobScheduler js = context.getSystemService(JobScheduler.class); 185 186 js.schedule(new JobInfo.Builder(JOB_IDLE_PROCESS, JOB_SERVICE_NAME) 187 .setRequiresDeviceIdle(true) 188 .setRequiresCharging(true) 189 .setPeriodic(BG_PROCESS_PERIOD) 190 .build()); 191 } 192 193 @Override onStartJob(JobParameters params)194 public boolean onStartJob(JobParameters params) { 195 if (DEBUG) { 196 Log.d(LOG_TAG, "Starting background process job"); 197 } 198 199 try { 200 sSelfService.mIProfcollect.process(false); 201 } catch (RemoteException e) { 202 Log.e(LOG_TAG, e.getMessage()); 203 } 204 return true; 205 } 206 207 @Override onStopJob(JobParameters params)208 public boolean onStopJob(JobParameters params) { 209 // TODO: Handle this? 210 return false; 211 } 212 } 213 214 // Event observers registerObservers()215 private void registerObservers() { 216 registerAppLaunchObserver(); 217 registerOTAObserver(); 218 } 219 220 private final AppLaunchObserver mAppLaunchObserver = new AppLaunchObserver(); registerAppLaunchObserver()221 private void registerAppLaunchObserver() { 222 ActivityTaskManagerInternal atmInternal = 223 LocalServices.getService(ActivityTaskManagerInternal.class); 224 ActivityMetricsLaunchObserverRegistry launchObserverRegistry = 225 atmInternal.getLaunchObserverRegistry(); 226 launchObserverRegistry.registerLaunchObserver(mAppLaunchObserver); 227 } 228 traceOnAppStart(String packageName)229 private void traceOnAppStart(String packageName) { 230 if (mIProfcollect == null) { 231 return; 232 } 233 234 // Sample for a fraction of app launches. 235 int traceFrequency = DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT, 236 "applaunch_trace_freq", 2); 237 int randomNum = ThreadLocalRandom.current().nextInt(100); 238 if (randomNum < traceFrequency) { 239 try { 240 if (DEBUG) { 241 Log.d(LOG_TAG, "Tracing on app launch event: " + packageName); 242 } 243 mIProfcollect.trace_once("applaunch"); 244 } catch (RemoteException e) { 245 Log.e(LOG_TAG, e.getMessage()); 246 } 247 } 248 } 249 250 private class AppLaunchObserver implements ActivityMetricsLaunchObserver { 251 @Override onIntentStarted(Intent intent, long timestampNanos)252 public void onIntentStarted(Intent intent, long timestampNanos) { 253 traceOnAppStart(intent.getPackage()); 254 } 255 256 @Override onIntentFailed()257 public void onIntentFailed() { 258 // Ignored 259 } 260 261 @Override onActivityLaunched(byte[] activity, int temperature)262 public void onActivityLaunched(byte[] activity, int temperature) { 263 // Ignored 264 } 265 266 @Override onActivityLaunchCancelled(byte[] abortingActivity)267 public void onActivityLaunchCancelled(byte[] abortingActivity) { 268 // Ignored 269 } 270 271 @Override onActivityLaunchFinished(byte[] finalActivity, long timestampNanos)272 public void onActivityLaunchFinished(byte[] finalActivity, long timestampNanos) { 273 // Ignored 274 } 275 276 @Override onReportFullyDrawn(byte[] activity, long timestampNanos)277 public void onReportFullyDrawn(byte[] activity, long timestampNanos) { 278 // Ignored 279 } 280 } 281 registerOTAObserver()282 private void registerOTAObserver() { 283 UpdateEngine updateEngine = new UpdateEngine(); 284 updateEngine.bind(new UpdateEngineCallback() { 285 @Override 286 public void onStatusUpdate(int status, float percent) { 287 if (DEBUG) { 288 Log.d(LOG_TAG, "Received OTA status update, status: " + status + ", percent: " 289 + percent); 290 } 291 292 if (status == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT) { 293 packProfileReport(); 294 } 295 } 296 297 @Override 298 public void onPayloadApplicationComplete(int errorCode) { 299 // Ignored 300 } 301 }); 302 } 303 packProfileReport()304 private void packProfileReport() { 305 if (mIProfcollect == null) { 306 return; 307 } 308 309 if (!getUploaderEnabledConfig(getContext())) { 310 return; 311 } 312 313 new Thread(() -> { 314 try { 315 Context context = getContext(); 316 final String uploaderPkg = getUploaderPackageName(context); 317 final String uploaderAction = getUploaderActionName(context); 318 String reportUuid = mIProfcollect.report(); 319 320 final int profileId = getBBProfileId(); 321 String reportDir = "/data/user/" + profileId 322 + "/com.google.android.apps.internal.betterbug/cache/"; 323 String reportPath = reportDir + reportUuid + ".zip"; 324 325 if (!Files.exists(Paths.get(reportDir))) { 326 Log.i(LOG_TAG, "Destination directory does not exist, abort upload."); 327 return; 328 } 329 330 Intent uploadIntent = 331 new Intent(uploaderAction) 332 .setPackage(uploaderPkg) 333 .putExtra("EXTRA_DESTINATION", "PROFCOLLECT") 334 .putExtra("EXTRA_PACKAGE_NAME", getContext().getPackageName()) 335 .putExtra("EXTRA_PROFILE_PATH", reportPath) 336 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 337 338 List<ResolveInfo> receivers = 339 context.getPackageManager().queryBroadcastReceivers(uploadIntent, 0); 340 if (receivers == null || receivers.isEmpty()) { 341 Log.i(LOG_TAG, "No one to receive upload intent, abort upload."); 342 return; 343 } 344 mIProfcollect.copy_report_to_bb(profileId, reportUuid); 345 context.sendBroadcast(uploadIntent); 346 mIProfcollect.delete_report(reportUuid); 347 } catch (RemoteException e) { 348 Log.e(LOG_TAG, e.getMessage()); 349 } 350 }).start(); 351 } 352 353 /** 354 * Get BetterBug's profile ID. It is the work profile ID, if it exists. Otherwise the system 355 * user ID. 356 * 357 * @return BetterBug's profile ID. 358 */ getBBProfileId()359 private int getBBProfileId() { 360 UserManager userManager = UserManager.get(getContext()); 361 int[] profiles = userManager.getProfileIds(UserHandle.USER_SYSTEM, false); 362 for (int p : profiles) { 363 if (userManager.getUserInfo(p).isManagedProfile()) { 364 return p; 365 } 366 } 367 return UserHandle.USER_SYSTEM; 368 } 369 getUploaderEnabledConfig(Context context)370 private boolean getUploaderEnabledConfig(Context context) { 371 return context.getResources().getBoolean( 372 R.bool.config_profcollectReportUploaderEnabled); 373 } 374 getUploaderPackageName(Context context)375 private String getUploaderPackageName(Context context) { 376 return context.getResources().getString( 377 R.string.config_defaultProfcollectReportUploaderApp); 378 } 379 getUploaderActionName(Context context)380 private String getUploaderActionName(Context context) { 381 return context.getResources().getString( 382 R.string.config_defaultProfcollectReportUploaderAction); 383 } 384 } 385