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