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.car.cartelemetryapp; 18 19 import android.app.Activity; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.ServiceConnection; 24 import android.os.Bundle; 25 import android.os.IBinder; 26 import android.os.PersistableBundle; 27 import android.os.RemoteException; 28 import android.view.Gravity; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.ViewGroup.LayoutParams; 32 import android.widget.Button; 33 import android.widget.PopupWindow; 34 import android.widget.RadioButton; 35 import android.widget.TextView; 36 37 import androidx.recyclerview.widget.DividerItemDecoration; 38 import androidx.recyclerview.widget.LinearLayoutManager; 39 import androidx.recyclerview.widget.RecyclerView; 40 41 import java.time.LocalDateTime; 42 import java.time.ZoneId; 43 import java.time.format.DateTimeFormatter; 44 import java.util.ArrayDeque; 45 import java.util.ArrayList; 46 import java.util.Deque; 47 import java.util.HashMap; 48 import java.util.List; 49 import java.util.Map; 50 51 public class CarTelemetryActivity extends Activity { 52 private static final int LOG_SIZE = 100; 53 private ICarMetricsCollectorService mService; 54 private TextView mConfigNameView; 55 private TextView mHistoryView; 56 private TextView mLogView; 57 private RecyclerView mRecyclerView; 58 private PopupWindow mConfigPopup; 59 private Button mPopupCloseButton; 60 private Button mConfigButton; 61 private ConfigListAdaptor mAdapter; 62 private List<IConfigData> mConfigData = new ArrayList<>(); 63 private Map<String, Integer> mConfigNameIndex = new HashMap<>(); 64 private Deque<String> mLogs = new ArrayDeque<>(); 65 private Map<String, List<PersistableBundle>> mBundleHistory = new HashMap<>(); 66 private Map<String, List<String>> mErrorHistory = new HashMap<>(); 67 private boolean mDataRadioSelected = true; 68 private IConfigData mSelectedInfoConfig = new IConfigData(); 69 private ServiceConnection mConnection = new ServiceConnection() { 70 @Override 71 public void onServiceConnected(ComponentName className, IBinder service) { 72 mService = ICarMetricsCollectorService.Stub.asInterface(service); 73 onServiceBound(); 74 } 75 76 @Override 77 public void onServiceDisconnected(ComponentName className) {} 78 }; 79 80 @Override onCreate(Bundle savedInstanceState)81 protected void onCreate(Bundle savedInstanceState) { 82 super.onCreate(savedInstanceState); 83 setContentView(R.layout.activity_main); 84 85 mConfigNameView = findViewById(R.id.config_name_text); 86 mHistoryView = findViewById(R.id.history_text); 87 mLogView = findViewById(R.id.log_text); 88 89 RadioButton dataRadio = findViewById(R.id.data_radio); 90 dataRadio.setOnClickListener(v -> { 91 mDataRadioSelected = true; 92 mHistoryView.setText(getBundleHistoryString(mSelectedInfoConfig.name)); 93 }); 94 95 RadioButton errorRadio = findViewById(R.id.error_radio); 96 errorRadio.setOnClickListener(v -> { 97 mDataRadioSelected = false; 98 mHistoryView.setText(getErrorHistoryString(mSelectedInfoConfig.name)); 99 }); 100 101 ViewGroup parent = findViewById(R.id.mainLayout); 102 View configsView = this.getLayoutInflater().inflate(R.layout.config_popup, parent, false); 103 mConfigPopup = new PopupWindow( 104 configsView, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 105 mConfigButton = findViewById(R.id.config_button); 106 mConfigButton.setOnClickListener(v -> { 107 mConfigPopup.showAtLocation(configsView, Gravity.CENTER, 0, 0); 108 }); 109 mPopupCloseButton = configsView.findViewById(R.id.popup_close_button); 110 mPopupCloseButton.setOnClickListener(v -> { 111 mConfigPopup.dismiss(); 112 }); 113 114 mRecyclerView = configsView.findViewById(R.id.config_list); 115 mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 116 mRecyclerView.addItemDecoration( 117 new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); 118 119 Intent intent = new Intent(this, CarMetricsCollectorService.class); 120 bindService(intent, mConnection, Context.BIND_AUTO_CREATE); 121 } 122 123 @Override onDestroy()124 protected void onDestroy() { 125 if (mConnection != null) { 126 unbindService(mConnection); 127 } 128 super.onDestroy(); 129 } 130 onServiceBound()131 private void onServiceBound() { 132 try { 133 printLog(mService.getLog()); 134 } catch (RemoteException e) { 135 throw new IllegalStateException( 136 "Failed to call ICarMetricsCollectorService.getLog.", e); 137 } 138 IConfigStateListener configStateListener = new IConfigStateListener.Stub() { 139 @Override 140 public void onConfigAdded(String configName) { 141 // Set config to checked 142 mConfigData.get(mConfigNameIndex.get(configName)).selected = true; 143 getMainExecutor().execute(() -> { 144 mAdapter.notifyItemChanged(mConfigNameIndex.get(configName)); 145 }); 146 printLog("Added config " + configName); 147 } 148 }; 149 try { 150 mService.setConfigStateListener(configStateListener); 151 } catch (RemoteException e) { 152 throw new IllegalStateException( 153 "Failed to call ICarMetricsCollectorService.setConfigStateListener.", e); 154 } 155 IResultListener resultListener = new IResultListener.Stub() { 156 @Override 157 public void onResult( 158 String metricsConfigName, 159 IConfigData configData, 160 PersistableBundle report, 161 String telemetryError) { 162 lazyInitHistories(metricsConfigName); 163 mConfigData.set(mConfigNameIndex.get(metricsConfigName), configData); 164 // Add to bundle and error histories 165 if (report != null) { 166 if (!mBundleHistory.containsKey(metricsConfigName)) { 167 mBundleHistory.put(metricsConfigName, new ArrayList<PersistableBundle>()); 168 } 169 mBundleHistory.get(metricsConfigName).add(report); 170 printLog("Received report for " + metricsConfigName); 171 } else { 172 if (!mErrorHistory.containsKey(metricsConfigName)) { 173 mErrorHistory.put(metricsConfigName, new ArrayList<String>()); 174 } 175 mErrorHistory.get(metricsConfigName).add(telemetryError); 176 printLog("Received error for " + metricsConfigName); 177 } 178 if (metricsConfigName.equals(mSelectedInfoConfig.name)) { 179 refreshHistory(); 180 } 181 } 182 }; 183 try { 184 mService.setResultListener(resultListener); 185 } catch (RemoteException e) { 186 throw new IllegalStateException( 187 "Failed to call ICarMetricsCollectorService.setResultListener.", e); 188 } 189 190 try { 191 mConfigData = mService.getConfigData(); 192 } catch (RemoteException e) { 193 throw new IllegalStateException( 194 "Failed to call ICarMetricsCollectorService.getConfigData.", e); 195 } 196 197 for (int i = 0; i < mConfigData.size(); i++) { 198 mConfigNameIndex.put(mConfigData.get(i).name, i); 199 } 200 if (mConfigData.size() != 0) { 201 mSelectedInfoConfig = mConfigData.get(0); // Default to display first config data 202 refreshHistory(); 203 } 204 205 mAdapter = new ConfigListAdaptor( 206 mConfigData, new AdaptorCallback()); 207 mRecyclerView.setAdapter(mAdapter); 208 } 209 210 /** Converts bundle to string. */ bundleToString(PersistableBundle bundle)211 private String bundleToString(PersistableBundle bundle) { 212 StringBuilder sb = new StringBuilder(); 213 for (String key : bundle.keySet()) { 214 sb.append("--") 215 .append(key) 216 .append(": ") 217 .append(bundle.get(key).toString()) 218 .append("\n"); 219 } 220 return sb.toString(); 221 } 222 223 /** Converts bundle history to string. */ getBundleHistoryString(String configName)224 private String getBundleHistoryString(String configName) { 225 StringBuilder sb = new StringBuilder(); 226 if (!mBundleHistory.containsKey(configName)) { 227 return ""; 228 } 229 for (PersistableBundle bundle : mBundleHistory.get(configName)) { 230 sb.append(bundleToString(bundle)).append("\n"); 231 } 232 return sb.toString(); 233 } 234 235 /** Converts error history to string. */ getErrorHistoryString(String configName)236 private String getErrorHistoryString(String configName) { 237 StringBuilder sb = new StringBuilder(); 238 if (!mErrorHistory.containsKey(configName)) { 239 return ""; 240 } 241 for (String error : mErrorHistory.get(configName)) { 242 sb.append(error).append("\n"); 243 } 244 return sb.toString(); 245 } 246 247 /** Refreshes the history view with the currently selected config's data. */ refreshHistory()248 private void refreshHistory() { 249 getMainExecutor().execute(() -> { 250 mConfigNameView.setText(mSelectedInfoConfig.name); 251 if (mDataRadioSelected) { 252 mHistoryView.setText(getBundleHistoryString(mSelectedInfoConfig.name)); 253 } else { 254 mHistoryView.setText(getErrorHistoryString(mSelectedInfoConfig.name)); 255 } 256 }); 257 } 258 259 /** Clears the config data and histories. Cleared on server side too. */ clearConfigData(IConfigData configData)260 private void clearConfigData(IConfigData configData) { 261 configData.onReadyTimes = 0; 262 configData.sentBytes = 0; 263 configData.errorCount = 0; 264 if (mBundleHistory.containsKey(configData.name)) { 265 mBundleHistory.get(configData.name).clear(); 266 } 267 if (mErrorHistory.containsKey(configData.name)) { 268 mErrorHistory.get(configData.name).clear(); 269 } 270 try { 271 mService.clearHistory(configData.name); 272 } catch (RemoteException e) { 273 throw new IllegalStateException( 274 "Failed to ICarMetricsCollectorService.clearHistory.", e); 275 } 276 } 277 278 /** Retrieves histories from service if not already present. */ lazyInitHistories(String configName)279 private void lazyInitHistories(String configName) { 280 if (!mBundleHistory.containsKey(configName)) { 281 try { 282 mBundleHistory.put(configName, mService.getBundleHistory(configName)); 283 } catch (RemoteException e) { 284 throw new IllegalStateException( 285 "Failed to call ICarMetricsCollectorService.getBundleHistory.", e); 286 } 287 } 288 if (!mErrorHistory.containsKey(configName)) { 289 try { 290 mErrorHistory.put(configName, mService.getErrorHistory(configName)); 291 } catch (RemoteException e) { 292 throw new IllegalStateException( 293 "Failed to call ICarMetricsCollectorService.getErrorHistory.", e); 294 } 295 } 296 } 297 298 /** Prints to log view the log with prefixed timestamp. */ printLog(String log)299 private void printLog(String log) { 300 String text = LocalDateTime.now(ZoneId.systemDefault()) 301 .format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")) + ": " + log; 302 if (mLogs.size() >= LOG_SIZE) { 303 // Remove oldest element 304 mLogs.pollLast(); 305 } 306 mLogs.addFirst(text); 307 getMainExecutor().execute(() -> { 308 mLogView.setText(String.join("\n", mLogs)); 309 }); 310 } 311 312 private class AdaptorCallback implements ConfigListAdaptor.Callback { 313 @Override onAddButtonClicked(IConfigData configData)314 public void onAddButtonClicked(IConfigData configData) { 315 try { 316 mService.addConfig(configData.name); 317 } catch (RemoteException e) { 318 throw new IllegalStateException( 319 "Failed to ICarMetricsCollectorService.addConfig.", e); 320 } 321 } 322 323 @Override onRemoveButtonClicked(IConfigData configData)324 public void onRemoveButtonClicked(IConfigData configData) { 325 try { 326 mService.removeConfig(configData.name); 327 } catch (RemoteException e) { 328 throw new IllegalStateException( 329 "Failed to ICarMetricsCollectorService.removeConfig.", e); 330 } 331 configData.selected = false; 332 getMainExecutor().execute(() -> { 333 mAdapter.notifyItemChanged(mConfigNameIndex.get(configData.name)); 334 }); 335 printLog("Removed config " + configData.name); 336 } 337 338 @Override onInfoButtonClicked(IConfigData configData)339 public void onInfoButtonClicked(IConfigData configData) { 340 mSelectedInfoConfig = configData; 341 mConfigNameView.setText(configData.name); 342 lazyInitHistories(configData.name); 343 getMainExecutor().execute(() -> { 344 if (mDataRadioSelected) { 345 mHistoryView.setText(getBundleHistoryString(configData.name)); 346 } else { 347 mHistoryView.setText(getErrorHistoryString(configData.name)); 348 } 349 }); 350 } 351 352 @Override onClearButtonClicked(IConfigData configData)353 public void onClearButtonClicked(IConfigData configData) { 354 int index = mConfigNameIndex.get(configData.name); 355 clearConfigData(configData); 356 getMainExecutor().execute(() -> { 357 mAdapter.notifyItemChanged(index); 358 }); 359 } 360 } 361 } 362