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.BroadcastReceiver;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.os.Handler;
29 import android.os.IBinder.DeathRecipient;
30 import android.os.Looper;
31 import android.os.RemoteException;
32 import android.os.ServiceManager;
33 import android.os.SystemProperties;
34 import android.os.UpdateEngine;
35 import android.os.UpdateEngineCallback;
36 import android.provider.DeviceConfig;
37 import android.provider.Settings;
38 import android.provider.Settings.SettingNotFoundException;
39 import android.util.Log;
40 
41 import com.android.internal.R;
42 import com.android.internal.os.BackgroundThread;
43 import com.android.server.IoThread;
44 import com.android.server.LocalServices;
45 import com.android.server.SystemService;
46 import com.android.server.wm.ActivityMetricsLaunchObserver;
47 import com.android.server.wm.ActivityMetricsLaunchObserverRegistry;
48 import com.android.server.wm.ActivityTaskManagerInternal;
49 
50 import java.util.concurrent.ThreadLocalRandom;
51 import java.util.concurrent.TimeUnit;
52 
53 /**
54  * System-server-local proxy into the {@code IProfcollectd} native service.
55  */
56 public final class ProfcollectForwardingService extends SystemService {
57     public static final String LOG_TAG = "ProfcollectForwardingService";
58 
59     private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
60     private static final String INTENT_UPLOAD_PROFILES =
61             "com.android.server.profcollect.UPLOAD_PROFILES";
62     private static final long BG_PROCESS_PERIOD = TimeUnit.HOURS.toMillis(4); // every 4 hours.
63 
64     private IProfCollectd mIProfcollect;
65     private static ProfcollectForwardingService sSelfService;
66     private final Handler mHandler = new ProfcollectdHandler(IoThread.getHandler().getLooper());
67 
68     private IProviderStatusCallback mProviderStatusCallback = new IProviderStatusCallback.Stub() {
69         public void onProviderReady() {
70             mHandler.sendEmptyMessage(ProfcollectdHandler.MESSAGE_REGISTER_SCHEDULERS);
71         }
72     };
73 
74     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
75         @Override
76         public void onReceive(Context context, Intent intent) {
77             if (INTENT_UPLOAD_PROFILES.equals(intent.getAction())) {
78                 Log.d(LOG_TAG, "Received broadcast to pack and upload reports");
79                 packAndUploadReport();
80             }
81         }
82     };
83 
ProfcollectForwardingService(Context context)84     public ProfcollectForwardingService(Context context) {
85         super(context);
86 
87         if (sSelfService != null) {
88             throw new AssertionError("only one service instance allowed");
89         }
90         sSelfService = this;
91 
92         final IntentFilter filter = new IntentFilter();
93         filter.addAction(INTENT_UPLOAD_PROFILES);
94         context.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
95     }
96 
97     /**
98      * Check whether profcollect is enabled through device config.
99      */
enabled()100     public static boolean enabled() {
101         return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT, "enabled",
102             false) || SystemProperties.getBoolean("persist.profcollectd.enabled_override", false);
103     }
104 
105     @Override
onStart()106     public void onStart() {
107         if (DEBUG) {
108             Log.d(LOG_TAG, "Profcollect forwarding service start");
109         }
110         connectNativeService();
111     }
112 
113     @Override
onBootPhase(int phase)114     public void onBootPhase(int phase) {
115         if (phase == PHASE_BOOT_COMPLETED) {
116             if (mIProfcollect == null) {
117                 return;
118             }
119             BackgroundThread.get().getThreadHandler().post(() -> {
120                 if (serviceHasSupportedTraceProvider()) {
121                     registerProviderStatusCallback();
122                 }
123             });
124         }
125     }
126 
registerProviderStatusCallback()127     private void registerProviderStatusCallback() {
128         if (mIProfcollect == null) {
129             return;
130         }
131         try {
132             mIProfcollect.registerProviderStatusCallback(mProviderStatusCallback);
133         } catch (RemoteException e) {
134             Log.e(LOG_TAG, "Failed to register provider status callback: " + e.getMessage());
135         }
136     }
137 
serviceHasSupportedTraceProvider()138     private boolean serviceHasSupportedTraceProvider() {
139         if (mIProfcollect == null) {
140             return false;
141         }
142         try {
143             return !mIProfcollect.get_supported_provider().isEmpty();
144         } catch (RemoteException e) {
145             Log.e(LOG_TAG, "Failed to get supported provider: " + e.getMessage());
146             return false;
147         }
148     }
149 
tryConnectNativeService()150     private boolean tryConnectNativeService() {
151         if (connectNativeService()) {
152             return true;
153         }
154         // Cannot connect to the native service at this time, retry after a short delay.
155         mHandler.sendEmptyMessageDelayed(ProfcollectdHandler.MESSAGE_BINDER_CONNECT, 5000);
156         return false;
157     }
158 
connectNativeService()159     private boolean connectNativeService() {
160         try {
161             IProfCollectd profcollectd =
162                     IProfCollectd.Stub.asInterface(
163                             ServiceManager.getServiceOrThrow("profcollectd"));
164             profcollectd.asBinder().linkToDeath(new ProfcollectdDeathRecipient(), /*flags*/0);
165             mIProfcollect = profcollectd;
166             return true;
167         } catch (ServiceManager.ServiceNotFoundException | RemoteException e) {
168             Log.w(LOG_TAG, "Failed to connect profcollectd binder service.");
169             return false;
170         }
171     }
172 
173     private class ProfcollectdHandler extends Handler {
ProfcollectdHandler(Looper looper)174         public ProfcollectdHandler(Looper looper) {
175             super(looper);
176         }
177 
178         public static final int MESSAGE_BINDER_CONNECT = 0;
179         public static final int MESSAGE_REGISTER_SCHEDULERS = 1;
180 
181         @Override
handleMessage(android.os.Message message)182         public void handleMessage(android.os.Message message) {
183             switch (message.what) {
184                 case MESSAGE_BINDER_CONNECT:
185                     connectNativeService();
186                     break;
187                 case MESSAGE_REGISTER_SCHEDULERS:
188                     registerObservers();
189                     ProfcollectBGJobService.schedule(getContext());
190                     break;
191                 default:
192                     throw new AssertionError("Unknown message: " + message);
193             }
194         }
195     }
196 
197     private class ProfcollectdDeathRecipient implements DeathRecipient {
198         @Override
binderDied()199         public void binderDied() {
200             Log.w(LOG_TAG, "profcollectd has died");
201 
202             mIProfcollect = null;
203             tryConnectNativeService();
204         }
205     }
206 
207     /**
208      * Background trace process service.
209      */
210     public static class ProfcollectBGJobService extends JobService {
211         // Unique ID in system service
212         private static final int JOB_IDLE_PROCESS = 260817;
213         private static final ComponentName JOB_SERVICE_NAME = new ComponentName(
214                 "android",
215                 ProfcollectBGJobService.class.getName());
216 
217         /**
218          * Attach the service to the system job scheduler.
219          */
schedule(Context context)220         public static void schedule(Context context) {
221             JobScheduler js = context.getSystemService(JobScheduler.class);
222 
223             js.schedule(new JobInfo.Builder(JOB_IDLE_PROCESS, JOB_SERVICE_NAME)
224                     .setRequiresDeviceIdle(true)
225                     .setRequiresCharging(true)
226                     .setPeriodic(BG_PROCESS_PERIOD)
227                     .setPriority(JobInfo.PRIORITY_MIN)
228                     .build());
229         }
230 
231         @Override
onStartJob(JobParameters params)232         public boolean onStartJob(JobParameters params) {
233             if (DEBUG) {
234                 Log.d(LOG_TAG, "Starting background process job");
235             }
236 
237             BackgroundThread.get().getThreadHandler().post(
238                     () -> {
239                         try {
240                             if (sSelfService.mIProfcollect == null) {
241                                 return;
242                             }
243                             sSelfService.mIProfcollect.process();
244                         } catch (RemoteException e) {
245                             Log.e(LOG_TAG, "Failed to process profiles in background: "
246                                     + e.getMessage());
247                         }
248                     });
249             return true;
250         }
251 
252         @Override
onStopJob(JobParameters params)253         public boolean onStopJob(JobParameters params) {
254             // TODO: Handle this?
255             return false;
256         }
257     }
258 
259     // Event observers
registerObservers()260     private void registerObservers() {
261         BackgroundThread.get().getThreadHandler().post(
262                 () -> {
263                     registerAppLaunchObserver();
264                     registerOTAObserver();
265                 });
266     }
267 
268     private final AppLaunchObserver mAppLaunchObserver = new AppLaunchObserver();
registerAppLaunchObserver()269     private void registerAppLaunchObserver() {
270         ActivityTaskManagerInternal atmInternal =
271                 LocalServices.getService(ActivityTaskManagerInternal.class);
272         ActivityMetricsLaunchObserverRegistry launchObserverRegistry =
273                 atmInternal.getLaunchObserverRegistry();
274         launchObserverRegistry.registerLaunchObserver(mAppLaunchObserver);
275     }
276 
traceOnAppStart(String packageName)277     private void traceOnAppStart(String packageName) {
278         if (mIProfcollect == null) {
279             return;
280         }
281 
282         // Sample for a fraction of app launches.
283         int traceFrequency = DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
284                 "applaunch_trace_freq", 2);
285         int randomNum = ThreadLocalRandom.current().nextInt(100);
286         if (randomNum < traceFrequency) {
287             if (DEBUG) {
288                 Log.d(LOG_TAG, "Tracing on app launch event: " + packageName);
289             }
290             BackgroundThread.get().getThreadHandler().post(() -> {
291                 try {
292                     mIProfcollect.trace_once("applaunch");
293                 } catch (RemoteException e) {
294                     Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
295                 }
296             });
297         }
298     }
299 
300     private class AppLaunchObserver extends ActivityMetricsLaunchObserver {
301         @Override
onIntentStarted(Intent intent, long timestampNanos)302         public void onIntentStarted(Intent intent, long timestampNanos) {
303             traceOnAppStart(intent.getPackage());
304         }
305     }
306 
registerOTAObserver()307     private void registerOTAObserver() {
308         UpdateEngine updateEngine = new UpdateEngine();
309         updateEngine.bind(new UpdateEngineCallback() {
310             @Override
311             public void onStatusUpdate(int status, float percent) {
312                 if (DEBUG) {
313                     Log.d(LOG_TAG, "Received OTA status update, status: " + status + ", percent: "
314                             + percent);
315                 }
316 
317                 if (status == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT) {
318                     packAndUploadReport();
319                 }
320             }
321 
322             @Override
323             public void onPayloadApplicationComplete(int errorCode) {
324                 // Ignored
325             }
326         });
327     }
328 
packAndUploadReport()329     private void packAndUploadReport() {
330         if (mIProfcollect == null) {
331             return;
332         }
333 
334         Context context = getContext();
335         BackgroundThread.get().getThreadHandler().post(() -> {
336             try {
337                 int usageSetting = -1;
338                 try {
339                     // Get "Usage & diagnostics" checkbox status. 1 is for enabled, 0 is for
340                     // disabled.
341                     usageSetting = Settings.Global.getInt(context.getContentResolver(), "multi_cb");
342                 } catch (SettingNotFoundException e) {
343                     Log.i(LOG_TAG, "Usage setting not found: " + e.getMessage());
344                 }
345 
346                 // Prepare profile report
347                 String reportName = mIProfcollect.report(usageSetting) + ".zip";
348 
349                 if (!context.getResources().getBoolean(
350                         R.bool.config_profcollectReportUploaderEnabled)) {
351                     Log.i(LOG_TAG, "Upload is not enabled.");
352                     return;
353                 }
354 
355                 // Upload the report
356                 Intent intent = new Intent()
357                         .setPackage("com.android.shell")
358                         .setAction("com.android.shell.action.PROFCOLLECT_UPLOAD")
359                         .putExtra("filename", reportName);
360                 context.sendBroadcast(intent);
361             } catch (RemoteException e) {
362                 Log.e(LOG_TAG, "Failed to upload report: " + e.getMessage());
363             }
364         });
365     }
366 }
367