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