1 /* 2 * Copyright (C) 2016 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 package com.google.android.car.kitchensink.cluster; 17 18 import android.annotation.Nullable; 19 import android.car.Car; 20 import android.car.Car.CarServiceLifecycleListener; 21 import android.car.CarAppFocusManager; 22 import android.car.CarNotConnectedException; 23 import android.car.cluster.navigation.NavigationState; 24 import android.car.cluster.navigation.NavigationState.Cue; 25 import android.car.cluster.navigation.NavigationState.Cue.CueElement; 26 import android.car.cluster.navigation.NavigationState.Destination; 27 import android.car.cluster.navigation.NavigationState.Destination.Traffic; 28 import android.car.cluster.navigation.NavigationState.Distance; 29 import android.car.cluster.navigation.NavigationState.Lane; 30 import android.car.cluster.navigation.NavigationState.Lane.LaneDirection; 31 import android.car.cluster.navigation.NavigationState.Maneuver; 32 import android.car.cluster.navigation.NavigationState.NavigationStateProto; 33 import android.car.cluster.navigation.NavigationState.Road; 34 import android.car.cluster.navigation.NavigationState.Step; 35 import android.car.cluster.navigation.NavigationState.Timestamp; 36 import android.car.navigation.CarNavigationStatusManager; 37 import android.content.pm.PackageManager; 38 import android.os.Bundle; 39 import android.util.Log; 40 import android.view.LayoutInflater; 41 import android.view.View; 42 import android.view.ViewGroup; 43 import android.widget.Button; 44 import android.widget.Toast; 45 46 import androidx.annotation.IdRes; 47 import androidx.annotation.NonNull; 48 import androidx.fragment.app.Fragment; 49 50 import com.google.android.car.kitchensink.R; 51 52 import java.io.BufferedReader; 53 import java.io.IOException; 54 import java.io.InputStream; 55 import java.io.InputStreamReader; 56 import java.util.Timer; 57 import java.util.TimerTask; 58 59 /** 60 * Contains functions to test instrument cluster API. 61 */ 62 public class InstrumentClusterFragment extends Fragment { 63 private static final String TAG = "Cluster.KitchenSink"; 64 65 private static final int DISPLAY_IN_CLUSTER_PERMISSION_REQUEST = 1; 66 67 private CarNavigationStatusManager mCarNavigationStatusManager; 68 private CarAppFocusManager mCarAppFocusManager; 69 private Car mCarApi; 70 private Timer mTimer; 71 private NavigationStateProto[] mNavStateData; 72 private Button mTurnByTurnButton; 73 74 private CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> { 75 if (!ready) { 76 Log.d(TAG, "Disconnect from Car Service"); 77 return; 78 } 79 Log.d(TAG, "Connected to Car Service"); 80 try { 81 mCarNavigationStatusManager = (CarNavigationStatusManager) car.getCarManager( 82 Car.CAR_NAVIGATION_SERVICE); 83 mCarAppFocusManager = (CarAppFocusManager) car.getCarManager( 84 Car.APP_FOCUS_SERVICE); 85 } catch (CarNotConnectedException e) { 86 Log.e(TAG, "Car is not connected!", e); 87 } 88 }; 89 90 private final CarAppFocusManager.OnAppFocusOwnershipCallback mFocusCallback = 91 new CarAppFocusManager.OnAppFocusOwnershipCallback() { 92 @Override 93 public void onAppFocusOwnershipLost(@CarAppFocusManager.AppFocusType int appType) { 94 if (Log.isLoggable(TAG, Log.DEBUG)) { 95 Log.d(TAG, "onAppFocusOwnershipLost, appType: " + appType); 96 } 97 Toast.makeText(getContext(), getText(R.string.cluster_nav_app_context_loss), 98 Toast.LENGTH_LONG).show(); 99 } 100 101 @Override 102 public void onAppFocusOwnershipGranted( 103 @CarAppFocusManager.AppFocusType int appType) { 104 if (Log.isLoggable(TAG, Log.DEBUG)) { 105 Log.d(TAG, "onAppFocusOwnershipGranted, appType: " + appType); 106 } 107 } 108 }; 109 private CarAppFocusManager.OnAppFocusChangedListener mOnAppFocusChangedListener = 110 (appType, active) -> { 111 if (Log.isLoggable(TAG, Log.DEBUG)) { 112 Log.d(TAG, "onAppFocusChanged, appType: " + appType + " active: " + active); 113 } 114 }; 115 116 initCarApi()117 private void initCarApi() { 118 mCarApi = Car.createCar(getContext(), /* handler= */ null, 119 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener); 120 } 121 122 @NonNull getNavStateData()123 private NavigationStateProto[] getNavStateData() { 124 NavigationStateProto[] navigationStateArray = new NavigationStateProto[1]; 125 126 navigationStateArray[0] = NavigationStateProto.newBuilder() 127 .addSteps(Step.newBuilder() 128 .setManeuver(Maneuver.newBuilder() 129 .setType(Maneuver.Type.DEPART) 130 .build()) 131 .setDistance(Distance.newBuilder() 132 .setMeters(300) 133 .setDisplayUnits(Distance.Unit.FEET) 134 .setDisplayValue("0.5") 135 .build()) 136 .setCue(Cue.newBuilder() 137 .addElements(CueElement.newBuilder() 138 .setText("Stay on ") 139 .build()) 140 .addElements(CueElement.newBuilder() 141 .setText("US 101 ") 142 .setImage(NavigationState.ImageReference.newBuilder() 143 .setAspectRatio(1.153846) 144 .setContentUri( 145 "content://com.google.android.car" 146 + ".kitchensink.cluster" 147 + ".clustercontentprovider/img" 148 + "/US_101.png") 149 .build()) 150 .build()) 151 .build()) 152 .addLanes(Lane.newBuilder() 153 .addLaneDirections(LaneDirection.newBuilder() 154 .setShape(LaneDirection.Shape.SLIGHT_LEFT) 155 .setIsHighlighted(false) 156 .build()) 157 .addLaneDirections(LaneDirection.newBuilder() 158 .setShape(LaneDirection.Shape.STRAIGHT) 159 .setIsHighlighted(true) 160 .build()) 161 .build()) 162 .build()) 163 .setCurrentRoad(Road.newBuilder() 164 .setName("On something really long st") 165 .build()) 166 .addDestinations(Destination.newBuilder() 167 .setTitle("Home") 168 .setAddress("123 Main st") 169 .setDistance(Distance.newBuilder() 170 .setMeters(2000) 171 .setDisplayValue("2") 172 .setDisplayUnits(Distance.Unit.KILOMETERS) 173 .build()) 174 .setEstimatedTimeAtArrival(Timestamp.newBuilder() 175 .setSeconds(1592610807) 176 .build()) 177 .setFormattedDurationUntilArrival("45 min") 178 .setZoneId("America/Los_Angeles") 179 .setTraffic(Traffic.HIGH) 180 .build()) 181 .build(); 182 183 return navigationStateArray; 184 } 185 186 /** 187 * Loads a raw resource as a single string. 188 */ 189 @NonNull getRawResourceAsString(@dRes int resId)190 private String getRawResourceAsString(@IdRes int resId) throws IOException { 191 InputStream inputStream = getResources().openRawResource(resId); 192 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 193 StringBuilder builder = new StringBuilder(); 194 for (String line; (line = reader.readLine()) != null; ) { 195 builder.append(line).append("\n"); 196 } 197 return builder.toString(); 198 } 199 200 @Nullable 201 @Override onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)202 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 203 @Nullable Bundle savedInstanceState) { 204 View view = inflater.inflate(R.layout.instrument_cluster, container, false); 205 206 view.findViewById(R.id.cluster_start_button).setOnClickListener(v -> initCluster()); 207 view.findViewById(R.id.cluster_stop_button).setOnClickListener(v -> stopCluster()); 208 209 mTurnByTurnButton = view.findViewById(R.id.cluster_turn_left_button); 210 mTurnByTurnButton.setOnClickListener(v -> toggleSendTurn()); 211 212 return view; 213 } 214 215 @Override onCreate(@ullable Bundle savedInstanceState)216 public void onCreate(@Nullable Bundle savedInstanceState) { 217 initCarApi(); 218 super.onCreate(savedInstanceState); 219 } 220 221 @Override onDestroy()222 public void onDestroy() { 223 if (mCarApi != null && mCarApi.isConnected()) { 224 mCarApi.disconnect(); 225 mCarApi = null; 226 } 227 super.onDestroy(); 228 } 229 230 /** 231 * Enables/disables sending turn-by-turn data through the {@link CarNavigationStatusManager} 232 */ toggleSendTurn()233 private void toggleSendTurn() { 234 // If we haven't yet load the sample navigation state data, do so. 235 if (mNavStateData == null) { 236 mNavStateData = getNavStateData(); 237 } 238 239 // Toggle a timer to send update periodically. 240 if (mTimer == null) { 241 startSendTurn(); 242 } else { 243 stopSendTurn(); 244 } 245 } 246 startSendTurn()247 private void startSendTurn() { 248 if (mTimer != null) { 249 stopSendTurn(); 250 } 251 if (!hasFocus()) { 252 Toast.makeText(getContext(), getText(R.string.cluster_not_started), Toast.LENGTH_LONG) 253 .show(); 254 return; 255 } 256 mTimer = new Timer(); 257 mTimer.schedule(new TimerTask() { 258 private int mPos; 259 260 @Override 261 public void run() { 262 sendTurn(mNavStateData[mPos]); 263 mPos = (mPos + 1) % mNavStateData.length; 264 } 265 }, 0, 1000); 266 mTurnByTurnButton.setText(R.string.cluster_stop_guidance); 267 } 268 stopSendTurn()269 private void stopSendTurn() { 270 if (mTimer != null) { 271 mTimer.cancel(); 272 mTimer = null; 273 } 274 mTurnByTurnButton.setText(R.string.cluster_start_guidance); 275 } 276 277 /** 278 * Sends one update of the navigation state through the {@link CarNavigationStatusManager} 279 */ sendTurn(@onNull NavigationStateProto state)280 private void sendTurn(@NonNull NavigationStateProto state) { 281 try { 282 Bundle bundle = new Bundle(); 283 bundle.putByteArray("navstate2", state.toByteArray()); 284 mCarNavigationStatusManager.sendEvent(1, bundle); 285 Log.i(TAG, "Sending nav state: " + state); 286 } catch (CarNotConnectedException e) { 287 Log.e(TAG, "Failed to send turn information.", e); 288 } 289 } 290 initCluster()291 private void initCluster() { 292 if (hasFocus()) { 293 Log.i(TAG, "Already has focus"); 294 return; 295 } 296 mCarAppFocusManager.addFocusListener(mOnAppFocusChangedListener, 297 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 298 mCarAppFocusManager.requestAppFocus(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, 299 mFocusCallback); 300 Log.i(TAG, "Focus requested"); 301 } 302 hasFocus()303 private boolean hasFocus() { 304 boolean ownsFocus = mCarAppFocusManager.isOwningFocus(mFocusCallback, 305 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 306 if (Log.isLoggable(TAG, Log.DEBUG)) { 307 Log.d(TAG, "Owns APP_FOCUS_TYPE_NAVIGATION: " + ownsFocus); 308 } 309 return ownsFocus; 310 } 311 stopCluster()312 private void stopCluster() { 313 stopSendTurn(); 314 mCarAppFocusManager.removeFocusListener(mOnAppFocusChangedListener, 315 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 316 mCarAppFocusManager.abandonAppFocus(mFocusCallback, 317 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 318 } 319 320 @Override onResume()321 public void onResume() { 322 super.onResume(); 323 Log.i(TAG, "onResume!"); 324 if (getActivity().checkSelfPermission(android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER) 325 != PackageManager.PERMISSION_GRANTED) { 326 Log.i(TAG, "Requesting: " + android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER); 327 328 requestPermissions(new String[]{android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER}, 329 DISPLAY_IN_CLUSTER_PERMISSION_REQUEST); 330 } else { 331 Log.i(TAG, "All required permissions granted"); 332 } 333 } 334 335 @Override onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)336 public void onRequestPermissionsResult(int requestCode, String[] permissions, 337 int[] grantResults) { 338 if (DISPLAY_IN_CLUSTER_PERMISSION_REQUEST == requestCode) { 339 for (int i = 0; i < permissions.length; i++) { 340 boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED; 341 Log.i(TAG, "onRequestPermissionsResult, requestCode: " + requestCode 342 + ", permission: " + permissions[i] + ", granted: " + granted); 343 } 344 } 345 } 346 } 347