1 /*
2  * Copyright (C) 2018 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 
17 package com.android.incallui.calllocation.impl;
18 
19 import android.animation.LayoutTransition;
20 import android.content.Context;
21 import android.location.Location;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.support.annotation.NonNull;
25 import android.text.TextUtils;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.TextView;
30 import android.widget.ViewAnimator;
31 import com.android.dialer.common.Assert;
32 import com.android.dialer.common.LogUtil;
33 import com.android.dialer.logging.DialerImpression;
34 import com.android.dialer.logging.Logger;
35 import com.android.incallui.baseui.BaseFragment;
36 import com.google.android.gms.maps.CameraUpdateFactory;
37 import com.google.android.gms.maps.GoogleMap;
38 import com.google.android.gms.maps.MapView;
39 import com.google.android.gms.maps.model.LatLng;
40 import com.google.android.gms.maps.model.MarkerOptions;
41 import java.util.Objects;
42 import java.util.concurrent.TimeUnit;
43 
44 /**
45  * Fragment which shows location during E911 calls, to supplement the user with accurate location
46  * information in case the user is asked for their location by the emergency responder.
47  *
48  * <p>If location data is inaccurate, stale, or unavailable, this should not be shown.
49  */
50 public class LocationFragment extends BaseFragment<LocationPresenter, LocationPresenter.LocationUi>
51     implements LocationPresenter.LocationUi {
52 
53   private static final String ADDRESS_DELIMITER = ",";
54 
55   // Indexes used to animate fading between views, 0 for LOADING_VIEW_INDEX
56   private static final int LOCATION_VIEW_INDEX = 1;
57   private static final int LOCATION_ERROR_INDEX = 2;
58 
59   private static final long FIND_LOCATION_SPINNING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
60   private static final long LOAD_DATA_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
61 
62   private static final float MAP_ZOOM_LEVEL = 15f;
63 
64   private ViewAnimator viewAnimator;
65   private MapView locationMapView;
66   private TextView addressLine1;
67   private TextView addressLine2;
68   private TextView latLongLine;
69   private Location location;
70   private ViewGroup locationLayout;
71   private GoogleMap savedGoogleMap;
72 
73   private boolean isMapSet;
74   private boolean isAddressSet;
75   private boolean isLocationSet;
76   private boolean hasTimeoutStarted;
77 
78   private final Handler handler = new Handler();
79   private final Runnable dataTimeoutRunnable =
80       () -> {
81         LogUtil.i(
82             "LocationFragment.dataTimeoutRunnable",
83             "timed out so animate any future layout changes");
84         locationLayout.setLayoutTransition(new LayoutTransition());
85         showLocationNow();
86       };
87 
88   private final Runnable spinningTimeoutRunnable =
89       () -> {
90         if (!(isAddressSet || isLocationSet || isMapSet)) {
91           // No data received, show error
92           viewAnimator.setDisplayedChild(LOCATION_ERROR_INDEX);
93         }
94       };
95 
96   @Override
createPresenter()97   public LocationPresenter createPresenter() {
98     return new LocationPresenter();
99   }
100 
101   @Override
getUi()102   public LocationPresenter.LocationUi getUi() {
103     return this;
104   }
105 
106   @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)107   public View onCreateView(
108       LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
109     LogUtil.enterBlock("LocationFragment.onCreateView");
110     final View view = inflater.inflate(R.layout.location_fragment, container, false);
111     viewAnimator = (ViewAnimator) view.findViewById(R.id.location_view_animator);
112     addressLine1 = (TextView) view.findViewById(R.id.address_line_one);
113     addressLine2 = (TextView) view.findViewById(R.id.address_line_two);
114     latLongLine = (TextView) view.findViewById(R.id.lat_long_line);
115     locationLayout = (ViewGroup) view.findViewById(R.id.location_layout);
116     locationMapView = (MapView) view.findViewById(R.id.location_map_view);
117     locationMapView.onCreate(savedInstanceState);
118     return view;
119   }
120 
121   @Override
onStart()122   public void onStart() {
123     super.onStart();
124     handler.postDelayed(spinningTimeoutRunnable, FIND_LOCATION_SPINNING_TIMEOUT_MILLIS);
125   }
126 
127   @Override
onDestroy()128   public void onDestroy() {
129     super.onDestroy();
130     handler.removeCallbacks(dataTimeoutRunnable);
131     handler.removeCallbacks(spinningTimeoutRunnable);
132   }
133 
setMap(@onNull Location location)134   private void setMap(@NonNull Location location) {
135     LogUtil.enterBlock("LocationFragment.setMap");
136     Assert.isNotNull(location);
137 
138     if (savedGoogleMap == null) {
139       locationMapView.getMapAsync(
140           (googleMap) -> {
141             LogUtil.enterBlock("LocationFragment.onMapReady");
142             savedGoogleMap = googleMap;
143             savedGoogleMap.getUiSettings().setMapToolbarEnabled(false);
144             updateMap(location);
145             isMapSet = true;
146             locationMapView.setVisibility(View.VISIBLE);
147 
148             // Hide Google logo
149             View child = locationMapView.getChildAt(0);
150             if (child instanceof ViewGroup) {
151               // Only the first child (View) is useful.
152               // Google logo can be in any other child (ViewGroup).
153               for (int i = 1; i < ((ViewGroup) child).getChildCount(); ++i) {
154                 ((ViewGroup) child).getChildAt(i).setVisibility(View.GONE);
155               }
156             }
157           });
158     } else {
159       updateMap(location);
160     }
161     displayWhenReady();
162     Logger.get(getContext()).logImpression(DialerImpression.Type.EMERGENCY_GOT_MAP);
163   }
164 
updateMap(@onNull Location location)165   private void updateMap(@NonNull Location location) {
166     Assert.isNotNull(location);
167     Assert.isNotNull(savedGoogleMap);
168     LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
169     savedGoogleMap.clear();
170     savedGoogleMap.addMarker(new MarkerOptions().position(latLng).flat(true).draggable(false));
171     savedGoogleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, MAP_ZOOM_LEVEL));
172   }
173 
174   @Override
setAddress(String address)175   public void setAddress(String address) {
176     LogUtil.i("LocationFragment.setAddress", address);
177     isAddressSet = true;
178     addressLine1.setVisibility(View.VISIBLE);
179     addressLine2.setVisibility(View.VISIBLE);
180     if (TextUtils.isEmpty(address)) {
181       addressLine1.setText(null);
182       addressLine2.setText(null);
183     } else {
184 
185       // Split the address after the first delimiter for display, if present.
186       // For example, "1600 Amphitheatre Parkway, Mountain View, CA 94043"
187       //     => "1600 Amphitheatre Parkway"
188       //     => "Mountain View, CA 94043"
189       int splitIndex = address.indexOf(ADDRESS_DELIMITER);
190       if (splitIndex >= 0) {
191         updateText(addressLine1, address.substring(0, splitIndex).trim());
192         updateText(addressLine2, address.substring(splitIndex + 1).trim());
193       } else {
194         updateText(addressLine1, address);
195         updateText(addressLine2, null);
196       }
197 
198       Logger.get(getContext()).logImpression(DialerImpression.Type.EMERGENCY_GOT_ADDRESS);
199     }
200     displayWhenReady();
201   }
202 
203   @Override
setLocation(Location location)204   public void setLocation(Location location) {
205     LogUtil.i("LocationFragment.setLocation", String.valueOf(location));
206     isLocationSet = true;
207     this.location = location;
208 
209     if (location != null) {
210       latLongLine.setVisibility(View.VISIBLE);
211       latLongLine.setText(
212           getContext()
213               .getString(
214                   R.string.lat_long_format, location.getLatitude(), location.getLongitude()));
215 
216       Logger.get(getContext()).logImpression(DialerImpression.Type.EMERGENCY_GOT_LOCATION);
217       setMap(location);
218     }
219     displayWhenReady();
220   }
221 
displayWhenReady()222   private void displayWhenReady() {
223     // Show the location if all data has loaded, otherwise prime the timeout
224     if (isMapSet && isAddressSet && isLocationSet) {
225       showLocationNow();
226     } else if (!hasTimeoutStarted) {
227       handler.postDelayed(dataTimeoutRunnable, LOAD_DATA_TIMEOUT_MILLIS);
228       hasTimeoutStarted = true;
229     }
230   }
231 
showLocationNow()232   private void showLocationNow() {
233     handler.removeCallbacks(dataTimeoutRunnable);
234     handler.removeCallbacks(spinningTimeoutRunnable);
235     if (viewAnimator.getDisplayedChild() != LOCATION_VIEW_INDEX) {
236       viewAnimator.setDisplayedChild(LOCATION_VIEW_INDEX);
237       viewAnimator.setOnClickListener(v -> launchMap());
238     }
239   }
240 
241   @Override
getContext()242   public Context getContext() {
243     return getActivity();
244   }
245 
launchMap()246   private void launchMap() {
247     if (location != null) {
248       startActivity(
249           LocationUrlBuilder.getShowMapIntent(
250               location, addressLine1.getText(), addressLine2.getText()));
251 
252       Logger.get(getContext()).logImpression(DialerImpression.Type.EMERGENCY_LAUNCHED_MAP);
253     }
254   }
255 
updateText(TextView view, String text)256   private static void updateText(TextView view, String text) {
257     if (!Objects.equals(text, view.getText())) {
258       view.setText(text);
259     }
260   }
261 
262   @Override
onResume()263   public void onResume() {
264     super.onResume();
265     locationMapView.onResume();
266   }
267 
268   @Override
onPause()269   public void onPause() {
270     locationMapView.onPause();
271     super.onPause();
272   }
273 }
274