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