1 /*
2  * Copyright (C) 2023 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.remoteaccess;
18 
19 import static android.preference.PreferenceManager.getDefaultSharedPreferences;
20 
21 import android.app.Service;
22 import android.car.Car;
23 import android.car.VehicleAreaSeat;
24 import android.car.VehiclePropertyIds;
25 import android.car.hardware.property.CarPropertyManager;
26 import android.car.remoteaccess.CarRemoteAccessManager;
27 import android.car.remoteaccess.RemoteTaskClientRegistrationInfo;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.os.Handler;
31 import android.os.HandlerThread;
32 import android.os.IBinder;
33 import android.util.Log;
34 
35 import com.android.internal.annotations.GuardedBy;
36 
37 import org.json.JSONArray;
38 import org.json.JSONException;
39 import org.json.JSONObject;
40 
41 import java.nio.charset.StandardCharsets;
42 import java.time.LocalDateTime;
43 import java.time.ZoneId;
44 import java.time.format.DateTimeFormatter;
45 import java.time.format.FormatStyle;
46 import java.util.concurrent.Executor;
47 
48 public final class KitchenSinkRemoteTaskService extends Service {
49 
50     private static final String TAG = KitchenSinkRemoteTaskService.class.getSimpleName();
51 
52     static final String PREF_KEY = "Tasks";
53     static final String TASK_TIME_KEY = "TaskTime";
54     static final String TASK_ID_KEY = "TaskId";
55     static final String TASK_DATA_KEY = "TaskData";
56     static final String TASK_DURATION_KEY = "TaskDuration";
57     private final HandlerThread mHandlerThread = new HandlerThread(getClass().getSimpleName());
58     private Handler mHandler;
59 
60     private static final String SET_TEMP = "SetTemp:";
61 
62     private final RemoteTaskClient mRemoteTaskClient = new RemoteTaskClient();
63 
64     private Car mCar;
65     private CarRemoteAccessManager mRemoteAccessManager;
66     private CarPropertyManager mCarPropertyManager;
67 
68     private final Object mLock = new Object();
69     @GuardedBy("mLock")
70     private SharedPreferences mSharedPref;
71 
72     @Override
onCreate()73     public void onCreate() {
74         Log.i(TAG, "onCreate");
75         mHandlerThread.start();
76         mHandler = new Handler(mHandlerThread.getLooper());
77         synchronized (mLock) {
78             mSharedPref = getDefaultSharedPreferences(this);
79         }
80         disconnectCar();
81         mCar = Car.createCar(this, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
82                 (Car car, boolean ready) -> {
83                     if (ready) {
84                         Executor executor = getMainExecutor();
85                         mRemoteAccessManager = (CarRemoteAccessManager) car.getCarManager(
86                                 Car.CAR_REMOTE_ACCESS_SERVICE);
87                         mRemoteAccessManager.setRemoteTaskClient(executor, mRemoteTaskClient);
88                         mCarPropertyManager = (CarPropertyManager) car.getCarManager(
89                                 Car.PROPERTY_SERVICE);
90                     } else {
91                         mCar = null;
92                         mRemoteAccessManager = null;
93                         mCarPropertyManager = null;
94                     }
95                 });
96     }
97 
98     @Override
onBind(Intent intent)99     public IBinder onBind(Intent intent) {
100         Log.i(TAG, "onBind");
101         return null;
102     }
103 
104     @Override
onDestroy()105     public void onDestroy() {
106         Log.i(TAG, "onDestroy");
107         disconnectCar();
108         mHandlerThread.quitSafely();
109     }
110 
disconnectCar()111     private void disconnectCar() {
112         if (mCar != null && mCar.isConnected()) {
113             mRemoteAccessManager.clearRemoteTaskClient();
114             mCar.disconnect();
115             mCar = null;
116         }
117     }
118 
handleTaskData(String taskData)119     private void handleTaskData(String taskData) {
120         if (taskData.startsWith(SET_TEMP)) {
121             float temp = Float.parseFloat(taskData.substring(SET_TEMP.length()));
122             Log.i(TAG, "Setting row 1 left HVAC temp to: " + temp);
123             try {
124                 mCarPropertyManager.setFloatProperty(
125                         VehiclePropertyIds.HVAC_TEMPERATURE_SET, VehicleAreaSeat.SEAT_ROW_1_LEFT,
126                         temp);
127             } catch (Exception e) {
128                 Log.e(TAG, "Failed to set hvac temp, error: " + e);
129             }
130         } else {
131             Log.i(TAG, "Unknown task data: " + taskData + ", do nothing");
132         }
133     }
134 
135     private final class RemoteTaskClient
136             implements CarRemoteAccessManager.RemoteTaskClientCallback {
137 
138         @Override
onRegistrationUpdated(RemoteTaskClientRegistrationInfo info)139         public void onRegistrationUpdated(RemoteTaskClientRegistrationInfo info) {
140             Log.i(TAG, "Registration information updated: serviceId=" + info.getServiceId()
141                     + ", vehicleId=" + info.getVehicleId() + ", processorId="
142                     + info.getProcessorId() + ", clientId=" + info.getClientId());
143         }
144 
145         @Override
onRegistrationFailed()146         public void onRegistrationFailed() {
147             Log.i(TAG, "Registration to CarRemoteAccessService failed");
148         }
149 
150         @Override
onRemoteTaskRequested(String taskId, byte[] data, int remainingTimeSec)151         public void onRemoteTaskRequested(String taskId, byte[] data, int remainingTimeSec) {
152             // Although in reality task data might not be a valid string, the test task data we use
153             // are always a valid string.
154             String taskDataStr = new String(data, StandardCharsets.UTF_8);
155 
156             // Lock to prevent concurrent access of shared pref.
157             synchronized (mLock) {
158                 Log.i(TAG, "Remote task(" + taskId + ") is requested with " + remainingTimeSec
159                         + " sec remaining, task data: " + taskDataStr);
160                 String taskListJson = mSharedPref.getString(
161                         KitchenSinkRemoteTaskService.PREF_KEY, "{}");
162                 SharedPreferences.Editor sharedPrefEditor = mSharedPref.edit();
163                 sharedPrefEditor.putString(PREF_KEY, toJsonString(
164                         taskListJson, taskId, taskDataStr, remainingTimeSec));
165                 sharedPrefEditor.apply();
166             }
167 
168             handleTaskData(taskDataStr);
169 
170             // Report task done after 5s.
171             mHandler.postDelayed(() -> {
172                 if (mRemoteAccessManager != null) {
173                     mRemoteAccessManager.reportRemoteTaskDone(taskId);
174                 }
175             }, /* delayMillis= */ 5000);
176         }
177 
178         @Override
onShutdownStarting(CarRemoteAccessManager.CompletableRemoteTaskFuture future)179         public void onShutdownStarting(CarRemoteAccessManager.CompletableRemoteTaskFuture future) {
180             mHandlerThread.quitSafely();
181             future.complete();
182         }
183 
formatTime(LocalDateTime t)184         private static String formatTime(LocalDateTime t) {
185             return t.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT));
186         }
187 
toJsonString(String taskListJson, String taskId, String taskDataStr, int remainingTimeSec)188         private static String toJsonString(String taskListJson, String taskId, String taskDataStr,
189                 int remainingTimeSec) {
190             LocalDateTime taskTime = LocalDateTime.now(ZoneId.systemDefault());
191             JSONObject jsonObject = null;
192             JSONArray tasks = null;
193             try {
194                 jsonObject = new JSONObject(taskListJson);
195                 tasks = (JSONArray) jsonObject.get(PREF_KEY);
196             } catch (JSONException e) {
197                 Log.w(TAG, "task list JSON is not initialized");
198             }
199             try {
200                 if (jsonObject == null || tasks == null) {
201                     jsonObject = new JSONObject();
202                     tasks = new JSONArray();
203                     jsonObject.put(PREF_KEY, tasks);
204                 }
205                 JSONObject task = new JSONObject();
206                 task.put(TASK_TIME_KEY, formatTime(taskTime));
207                 task.put(TASK_ID_KEY, taskId);
208                 task.put(TASK_DATA_KEY, taskDataStr);
209                 task.put(TASK_DURATION_KEY, Integer.valueOf(remainingTimeSec));
210                 tasks.put(task);
211             } catch (JSONException e) {
212                 Log.e(TAG, "Failed to convert task info to JSON", e);
213                 return "";
214             }
215             return jsonObject.toString();
216         }
217     }
218 }
219