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