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