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