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.google.android.car.kitchensink;
18 
19 import android.annotation.NonNull;
20 import android.car.Car;
21 import android.car.watchdog.CarWatchdogManager;
22 import android.car.watchdog.CarWatchdogManager.CarWatchdogClientCallback;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.os.SystemClock;
26 import android.util.Log;
27 
28 import java.util.concurrent.ExecutorService;
29 import java.util.concurrent.Executors;
30 
31 public final class CarWatchdogClient {
32     private static final String TAG = CarWatchdogClient.class.getSimpleName();
33     private static final String TIMEOUT_CRITICAL = "critical";
34     private static final String TIMEOUT_MODERATE = "moderate";
35     private static final String TIMEOUT_NORMAL = "normal";
36     private static CarWatchdogClient sCarWatchdogClient;
37 
38     private final CarWatchdogManager mCarWatchdogManager;
39     private final CarWatchdogClientCallback mClientCallback = new CarWatchdogClientCallback() {
40         @Override
41         public boolean onCheckHealthStatus(int sessionId, int timeout) {
42             if (mClientConfig.verbose) {
43                 Log.i(TAG, "onCheckHealthStatus: session id =  " + sessionId);
44             }
45             long currentUptime = SystemClock.uptimeMillis();
46             return mClientConfig.notRespondAfterInMs < 0
47                     || mClientConfig.notRespondAfterInMs > currentUptime - mClientStartTime;
48         }
49 
50         @Override
51         public void onPrepareProcessTermination() {
52             Log.w(TAG, "This process is being terminated by car watchdog");
53         }
54     };
55     private final ExecutorService mCallbackExecutor = Executors.newFixedThreadPool(1);
56     private ClientConfig mClientConfig;
57     private long mClientStartTime;
58 
59     // This method is not intended for multi-threaded calls.
start(Car car, @NonNull String command)60     public static void start(Car car, @NonNull String command) {
61         if (sCarWatchdogClient != null) {
62             Log.w(TAG, "Car watchdog client already started");
63             return;
64         }
65         ClientConfig config;
66         try {
67             config = parseCommand(command);
68         } catch (IllegalArgumentException e) {
69             Log.w(TAG, "Watchdog command error: " + e);
70             return;
71         }
72         sCarWatchdogClient = new CarWatchdogClient(car, config);
73         sCarWatchdogClient.registerAndGo();
74     }
75 
parseCommand(String command)76     private static ClientConfig parseCommand(String command) {
77         String[] tokens = command.split("[ ]+");
78         int paramCount = tokens.length;
79         if (paramCount != 3 && paramCount != 4) {
80             throw new IllegalArgumentException("invalid command syntax");
81         }
82         int timeout;
83         int inactiveMainAfterInSec;
84         int notRespondAfterInSec;
85         switch (tokens[0]) {
86             case TIMEOUT_CRITICAL:
87                 timeout = CarWatchdogManager.TIMEOUT_CRITICAL;
88                 break;
89             case TIMEOUT_MODERATE:
90                 timeout = CarWatchdogManager.TIMEOUT_MODERATE;
91                 break;
92             case TIMEOUT_NORMAL:
93                 timeout = CarWatchdogManager.TIMEOUT_NORMAL;
94                 break;
95             default:
96                 throw new IllegalArgumentException("invalid timeout value");
97         }
98         try {
99             notRespondAfterInSec = Integer.parseInt(tokens[1]);
100         } catch (NumberFormatException e) {
101             throw new IllegalArgumentException("time for \"not responding after\" is not number");
102         }
103         try {
104             inactiveMainAfterInSec = Integer.parseInt(tokens[2]);
105         } catch (NumberFormatException e) {
106             throw new IllegalArgumentException("time for \"inactive main after\" is not number");
107         }
108         boolean verbose = false;
109         if (paramCount == 4) {
110             switch (tokens[3]) {
111                 case "true":
112                     verbose = true;
113                     break;
114                 case "false":
115                     verbose = false;
116                     break;
117                 default:
118                     throw new IllegalArgumentException("invalid verbose value: " + tokens[3]);
119             }
120         }
121         Log.i(TAG, "CarWatchdogClient command: timeout = " + tokens[0] + ", notRespondingAfter = "
122                 + notRespondAfterInSec + ", inactiveMainAfter = " + inactiveMainAfterInSec
123                 + ", verbose = " + verbose);
124         return new ClientConfig(timeout, inactiveMainAfterInSec, notRespondAfterInSec, verbose);
125     }
126 
CarWatchdogClient(Car car, ClientConfig config)127     private CarWatchdogClient(Car car, ClientConfig config) {
128         mClientConfig = config;
129         mCarWatchdogManager = (CarWatchdogManager) car.getCarManager(Car.CAR_WATCHDOG_SERVICE);
130     }
131 
registerAndGo()132     private void registerAndGo() {
133         mClientStartTime = SystemClock.uptimeMillis();
134         mCarWatchdogManager.registerClient(mCallbackExecutor, mClientCallback,
135                 mClientConfig.timeout);
136         // Post a runnable which takes long time to finish to the main thread if inactive_main_after
137         // is no less than 0
138         if (mClientConfig.inactiveMainAfterInMs >= 0) {
139             Handler handler = new Handler(Looper.getMainLooper());
140             handler.postDelayed(() -> {
141                 try {
142                     if (mClientConfig.verbose) {
143                         Log.i(TAG, "Main thread gets inactive");
144                     }
145                     Thread.sleep(getTimeForInactiveMain(mClientConfig.timeout));
146                 } catch (InterruptedException e) {
147                     // Ignore
148                 }
149             }, mClientConfig.inactiveMainAfterInMs);
150         }
151     }
152 
153     // The waiting time = (timeout * 2) + 50 milliseconds.
getTimeForInactiveMain(int timeout)154     private long getTimeForInactiveMain(int timeout) {
155         switch (timeout) {
156             case CarWatchdogManager.TIMEOUT_CRITICAL:
157                 return 6050L;
158             case CarWatchdogManager.TIMEOUT_MODERATE:
159                 return 10050L;
160             case CarWatchdogManager.TIMEOUT_NORMAL:
161                 return 20050L;
162             default:
163                 Log.w(TAG, "Invalid timeout");
164                 return 20050L;
165         }
166     }
167 
168     private static final class ClientConfig {
169         public int timeout;
170         public long inactiveMainAfterInMs;
171         public long notRespondAfterInMs;
172         public boolean verbose;
173 
ClientConfig(int timeout, int inactiveMainAfterInSec, int notRespondAfterInSec, boolean verbose)174         ClientConfig(int timeout, int inactiveMainAfterInSec, int notRespondAfterInSec,
175                 boolean verbose) {
176             this.timeout = timeout;
177             inactiveMainAfterInMs = inactiveMainAfterInSec * 1000L;
178             notRespondAfterInMs = notRespondAfterInSec * 1000L;
179             this.verbose = verbose;
180         }
181     }
182 }
183