• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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