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