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 try (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 204 @Nullable 205 @Override onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)206 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 207 @Nullable Bundle savedInstanceState) { 208 View view = inflater.inflate(R.layout.instrument_cluster, container, false); 209 210 view.findViewById(R.id.cluster_start_button).setOnClickListener(v -> initCluster()); 211 view.findViewById(R.id.cluster_stop_button).setOnClickListener(v -> stopCluster()); 212 view.findViewById(R.id.cluster_activity_state_default).setOnClickListener(v -> 213 changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)); 214 view.findViewById(R.id.cluster_activity_state_enabled).setOnClickListener(v -> 215 changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_ENABLED)); 216 view.findViewById(R.id.cluster_activity_state_disabled).setOnClickListener(v -> 217 changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_DISABLED)); 218 updateInitialClusterActivityState(view); 219 220 mTurnByTurnButton = view.findViewById(R.id.cluster_turn_left_button); 221 mTurnByTurnButton.setOnClickListener(v -> toggleSendTurn()); 222 223 return view; 224 } 225 updateInitialClusterActivityState(View view)226 private void updateInitialClusterActivityState(View view) { 227 PackageManager pm = getContext().getPackageManager(); 228 ComponentName clusterActivity = 229 new ComponentName(getContext(), FakeClusterNavigationActivity.class); 230 int currentComponentState = pm.getComponentEnabledSetting(clusterActivity); 231 RadioButton button = view.findViewById( 232 convertClusterActivityStateToViewId(currentComponentState)); 233 button.setChecked(true); 234 } 235 convertClusterActivityStateToViewId(int componentState)236 private int convertClusterActivityStateToViewId(int componentState) { 237 switch (componentState) { 238 case PackageManager.COMPONENT_ENABLED_STATE_DEFAULT: 239 return R.id.cluster_activity_state_default; 240 case PackageManager.COMPONENT_ENABLED_STATE_ENABLED: 241 return R.id.cluster_activity_state_enabled; 242 case PackageManager.COMPONENT_ENABLED_STATE_DISABLED: 243 return R.id.cluster_activity_state_disabled; 244 } 245 throw new IllegalStateException("Unknown component state: " + componentState); 246 } 247 changeClusterActivityState(int newComponentState)248 private void changeClusterActivityState(int newComponentState) { 249 PackageManager pm = getContext().getPackageManager(); 250 ComponentName clusterActivity = 251 new ComponentName(getContext(), FakeClusterNavigationActivity.class); 252 pm.setComponentEnabledSetting(clusterActivity, newComponentState, 253 PackageManager.DONT_KILL_APP); 254 } 255 256 @Override onCreate(@ullable Bundle savedInstanceState)257 public void onCreate(@Nullable Bundle savedInstanceState) { 258 initCarApi(); 259 super.onCreate(savedInstanceState); 260 } 261 262 @Override onDestroy()263 public void onDestroy() { 264 if (mCarApi != null && mCarApi.isConnected()) { 265 mCarApi.disconnect(); 266 mCarApi = null; 267 } 268 super.onDestroy(); 269 } 270 271 /** 272 * Enables/disables sending turn-by-turn data through the {@link CarNavigationStatusManager} 273 */ toggleSendTurn()274 private void toggleSendTurn() { 275 // If we haven't yet load the sample navigation state data, do so. 276 if (mNavStateData == null) { 277 mNavStateData = getNavStateData(); 278 } 279 280 // Toggle a timer to send update periodically. 281 if (mTimer == null) { 282 startSendTurn(); 283 } else { 284 stopSendTurn(); 285 } 286 } 287 startSendTurn()288 private void startSendTurn() { 289 if (mTimer != null) { 290 stopSendTurn(); 291 } 292 if (!hasFocus()) { 293 Toast.makeText(getContext(), getText(R.string.cluster_not_started), Toast.LENGTH_LONG) 294 .show(); 295 return; 296 } 297 mTimer = new Timer(); 298 mTimer.schedule(new TimerTask() { 299 private int mPos; 300 301 @Override 302 public void run() { 303 sendTurn(mNavStateData[mPos]); 304 mPos = (mPos + 1) % mNavStateData.length; 305 } 306 }, 0, 1000); 307 mTurnByTurnButton.setText(R.string.cluster_stop_guidance); 308 } 309 stopSendTurn()310 private void stopSendTurn() { 311 if (mTimer != null) { 312 mTimer.cancel(); 313 mTimer = null; 314 } 315 sendTurn(NavigationStateProto.newBuilder().build()); 316 mTurnByTurnButton.setText(R.string.cluster_start_guidance); 317 } 318 319 /** 320 * Sends one update of the navigation state through the {@link CarNavigationStatusManager} 321 */ sendTurn(@onNull NavigationStateProto state)322 private void sendTurn(@NonNull NavigationStateProto state) { 323 if (hasFocus()) { 324 Bundle bundle = new Bundle(); 325 bundle.putByteArray("navstate2", state.toByteArray()); 326 mCarNavigationStatusManager.sendNavigationStateChange(bundle); 327 Log.i(TAG, "Sending nav state: " + state); 328 } 329 } 330 initCluster()331 private void initCluster() { 332 if (hasFocus()) { 333 Log.i(TAG, "Already has focus"); 334 return; 335 } 336 mCarAppFocusManager.addFocusListener(mOnAppFocusChangedListener, 337 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 338 mCarAppFocusManager.requestAppFocus(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, 339 mFocusCallback); 340 Log.i(TAG, "Focus requested"); 341 } 342 hasFocus()343 private boolean hasFocus() { 344 boolean ownsFocus = mCarAppFocusManager.isOwningFocus(mFocusCallback, 345 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 346 if (Log.isLoggable(TAG, Log.DEBUG)) { 347 Log.d(TAG, "Owns APP_FOCUS_TYPE_NAVIGATION: " + ownsFocus); 348 } 349 return ownsFocus; 350 } 351 stopCluster()352 private void stopCluster() { 353 stopSendTurn(); 354 mCarAppFocusManager.removeFocusListener(mOnAppFocusChangedListener, 355 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 356 mCarAppFocusManager.abandonAppFocus(mFocusCallback, 357 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 358 } 359 360 @Override onResume()361 public void onResume() { 362 super.onResume(); 363 Log.i(TAG, "onResume!"); 364 if (getActivity().checkSelfPermission(android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER) 365 != PackageManager.PERMISSION_GRANTED) { 366 Log.i(TAG, "Requesting: " + android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER); 367 368 requestPermissions(new String[]{android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER}, 369 DISPLAY_IN_CLUSTER_PERMISSION_REQUEST); 370 } else { 371 Log.i(TAG, "All required permissions granted"); 372 } 373 } 374 375 @Override onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)376 public void onRequestPermissionsResult(int requestCode, String[] permissions, 377 int[] grantResults) { 378 if (DISPLAY_IN_CLUSTER_PERMISSION_REQUEST == requestCode) { 379 for (int i = 0; i < permissions.length; i++) { 380 boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED; 381 Log.i(TAG, "onRequestPermissionsResult, requestCode: " + requestCode 382 + ", permission: " + permissions[i] + ", granted: " + granted); 383 } 384 } 385 } 386 } 387