summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/calllocation/impl
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/incallui/calllocation/impl')
-rw-r--r--java/com/android/incallui/calllocation/impl/AndroidManifest.xml26
-rw-r--r--java/com/android/incallui/calllocation/impl/AuthException.java25
-rw-r--r--java/com/android/incallui/calllocation/impl/CallLocationImpl.java67
-rw-r--r--java/com/android/incallui/calllocation/impl/CallLocationModule.java29
-rw-r--r--java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java77
-rw-r--r--java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java123
-rw-r--r--java/com/android/incallui/calllocation/impl/HttpFetcher.java289
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationFragment.java197
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationHelper.java219
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationPresenter.java98
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java177
-rw-r--r--java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java144
-rw-r--r--java/com/android/incallui/calllocation/impl/TrafficStatsTags.java29
-rw-r--r--java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml134
-rw-r--r--java/com/android/incallui/calllocation/impl/res/values/dimens.xml6
-rw-r--r--java/com/android/incallui/calllocation/impl/res/values/strings.xml15
-rw-r--r--java/com/android/incallui/calllocation/impl/res/values/styles.xml28
17 files changed, 1683 insertions, 0 deletions
diff --git a/java/com/android/incallui/calllocation/impl/AndroidManifest.xml b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml
new file mode 100644
index 000000000..550c5808c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.incallui.calllocation.impl">
+
+ <application>
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version"/>
+ </application>
+</manifest>
diff --git a/java/com/android/incallui/calllocation/impl/AuthException.java b/java/com/android/incallui/calllocation/impl/AuthException.java
new file mode 100644
index 000000000..26def2fc9
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/AuthException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+/** For detecting backend authorization errors */
+public class AuthException extends Exception {
+
+ public AuthException(String detailMessage) {
+ super(detailMessage);
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/CallLocationImpl.java b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java
new file mode 100644
index 000000000..20f5ffb0f
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.calllocation.CallLocation;
+import javax.inject.Inject;
+
+/** Uses Google Play Services to show the user's location during an emergency call. */
+public class CallLocationImpl implements CallLocation {
+
+ private LocationHelper locationHelper;
+ private LocationFragment locationFragment;
+
+ @Inject
+ public CallLocationImpl() {}
+
+ @MainThread
+ @Override
+ public boolean canGetLocation(@NonNull Context context) {
+ Assert.isMainThread();
+ return LocationHelper.canGetLocation(context);
+ }
+
+ @MainThread
+ @NonNull
+ @Override
+ public Fragment getLocationFragment(@NonNull Context context) {
+ Assert.isMainThread();
+ if (locationFragment == null) {
+ locationFragment = new LocationFragment();
+ locationHelper = new LocationHelper(context);
+ locationHelper.addLocationListener(locationFragment.getPresenter());
+ }
+ return locationFragment;
+ }
+
+ @MainThread
+ @Override
+ public void close() {
+ Assert.isMainThread();
+ if (locationFragment != null) {
+ locationHelper.removeLocationListener(locationFragment.getPresenter());
+ locationHelper.close();
+ locationFragment = null;
+ locationHelper = null;
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/CallLocationModule.java b/java/com/android/incallui/calllocation/impl/CallLocationModule.java
new file mode 100644
index 000000000..73e85554e
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/CallLocationModule.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import com.android.incallui.calllocation.CallLocation;
+import dagger.Binds;
+import dagger.Module;
+
+/** This module provides an instance of call location. */
+@Module
+public abstract class CallLocationModule {
+
+ @Binds
+ public abstract CallLocation bindCallLocation(CallLocationImpl callLocation);
+}
diff --git a/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java
new file mode 100644
index 000000000..801b0d35c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+
+class DownloadMapImageTask extends AsyncTask<Location, Void, Drawable> {
+
+ private static final String STATIC_MAP_SRC_NAME = "src";
+
+ private final WeakReference<LocationUi> mUiReference;
+
+ public DownloadMapImageTask(WeakReference<LocationUi> uiReference) {
+ mUiReference = uiReference;
+ }
+
+ @Override
+ protected Drawable doInBackground(Location... locations) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return null;
+ }
+ if (locations == null || locations.length == 0) {
+ LogUtil.e("DownloadMapImageTask.doInBackground", "No location provided");
+ return null;
+ }
+
+ try {
+ URL mapUrl = new URL(LocationUrlBuilder.getStaticMapUrl(ui.getContext(), locations[0]));
+ InputStream content = (InputStream) mapUrl.getContent();
+
+ TrafficStats.setThreadStatsTag(TrafficStatsTags.DOWNLOAD_LOCATION_MAP_TAG);
+ return Drawable.createFromStream(content, STATIC_MAP_SRC_NAME);
+ } catch (Exception ex) {
+ LogUtil.e("DownloadMapImageTask.doInBackground", "Exception!!!", ex);
+ return null;
+ } finally {
+ TrafficStats.clearThreadStatsTag();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Drawable mapImage) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return;
+ }
+
+ try {
+ ui.setMap(mapImage);
+ } catch (Exception ex) {
+ LogUtil.e("DownloadMapImageTask.onPostExecute", "Exception!!!", ex);
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
new file mode 100644
index 000000000..18a80b8ce
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Settings.Secure;
+import android.provider.Settings.SettingNotFoundException;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * Helper class to check if Google Location Services is enabled. This class is based on
+ * https://docs.google.com/a/google.com/document/d/1sGm8pHgGY1QmxbLCwTZuWQASEDN7CFW9EPSZXAuGQfo
+ */
+public class GoogleLocationSettingHelper {
+
+ /** User has disagreed to use location for Google services. */
+ public static final int USE_LOCATION_FOR_SERVICES_OFF = 0;
+ /** User has agreed to use location for Google services. */
+ public static final int USE_LOCATION_FOR_SERVICES_ON = 1;
+ /** The user has neither agreed nor disagreed to use location for Google services yet. */
+ public static final int USE_LOCATION_FOR_SERVICES_NOT_SET = 2;
+
+ private static final String GOOGLE_SETTINGS_AUTHORITY = "com.google.settings";
+ private static final Uri GOOGLE_SETTINGS_CONTENT_URI =
+ Uri.parse("content://" + GOOGLE_SETTINGS_AUTHORITY + "/partner");
+ private static final String NAME = "name";
+ private static final String VALUE = "value";
+ private static final String USE_LOCATION_FOR_SERVICES = "use_location_for_services";
+
+ /** Determine if Google apps need to conform to the USE_LOCATION_FOR_SERVICES setting. */
+ public static boolean isEnforceable(Context context) {
+ final ResolveInfo ri =
+ context
+ .getPackageManager()
+ .resolveActivity(
+ new Intent("com.google.android.gsf.GOOGLE_APPS_LOCATION_SETTINGS"),
+ PackageManager.MATCH_DEFAULT_ONLY);
+ return ri != null;
+ }
+
+ /**
+ * Get the current value for the 'Use value for location' setting.
+ *
+ * @return One of {@link #USE_LOCATION_FOR_SERVICES_NOT_SET}, {@link
+ * #USE_LOCATION_FOR_SERVICES_OFF} or {@link #USE_LOCATION_FOR_SERVICES_ON}.
+ */
+ private static int getUseLocationForServices(Context context) {
+ final ContentResolver resolver = context.getContentResolver();
+ Cursor c = null;
+ String stringValue = null;
+ try {
+ c =
+ resolver.query(
+ GOOGLE_SETTINGS_CONTENT_URI,
+ new String[] {VALUE},
+ NAME + "=?",
+ new String[] {USE_LOCATION_FOR_SERVICES},
+ null);
+ if (c != null && c.moveToNext()) {
+ stringValue = c.getString(0);
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "GoogleLocationSettingHelper.getUseLocationForServices",
+ "Failed to get 'Use My Location' setting",
+ e);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ if (stringValue == null) {
+ return USE_LOCATION_FOR_SERVICES_NOT_SET;
+ }
+ int value;
+ try {
+ value = Integer.parseInt(stringValue);
+ } catch (final NumberFormatException nfe) {
+ value = USE_LOCATION_FOR_SERVICES_NOT_SET;
+ }
+ return value;
+ }
+
+ /** Whether or not the system location setting is enable */
+ public static boolean isSystemLocationSettingEnabled(Context context) {
+ try {
+ return Secure.getInt(context.getContentResolver(), Secure.LOCATION_MODE)
+ != Secure.LOCATION_MODE_OFF;
+ } catch (SettingNotFoundException e) {
+ LogUtil.e(
+ "GoogleLocationSettingHelper.isSystemLocationSettingEnabled",
+ "Failed to get System Location setting",
+ e);
+ return false;
+ }
+ }
+
+ /** Convenience method that returns true is GLS is ON or if it's not enforceable. */
+ public static boolean isGoogleLocationServicesEnabled(Context context) {
+ return !isEnforceable(context)
+ || getUseLocationForServices(context) == USE_LOCATION_FOR_SERVICES_ON;
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/HttpFetcher.java b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
new file mode 100644
index 000000000..7bfbaa6ef
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import static com.android.dialer.util.DialerUtils.closeQuietly;
+
+import android.content.Context;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.SystemClock;
+import android.util.Pair;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.MoreStrings;
+import com.google.android.common.http.UrlRules;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/** Utility for making http requests. */
+public class HttpFetcher {
+
+ // Phone number
+ public static final String PARAM_ID = "id";
+ // auth token
+ public static final String PARAM_ACCESS_TOKEN = "access_token";
+ private static final String TAG = HttpFetcher.class.getSimpleName();
+
+ /**
+ * Send a http request to the given url.
+ *
+ * @param urlString The url to request.
+ * @return The response body as a byte array. Or {@literal null} if status code is not 2xx.
+ * @throws java.io.IOException when an error occurs.
+ */
+ public static byte[] sendRequestAsByteArray(
+ Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+ throws IOException, AuthException {
+ Objects.requireNonNull(urlString);
+
+ URL url = reWriteUrl(context, urlString);
+ if (url == null) {
+ return null;
+ }
+
+ HttpURLConnection conn = null;
+ InputStream is = null;
+ boolean isError = false;
+ final long start = SystemClock.uptimeMillis();
+ try {
+ conn = (HttpURLConnection) url.openConnection();
+ setMethodAndHeaders(conn, requestMethod, headers);
+ int responseCode = conn.getResponseCode();
+ LogUtil.i("HttpFetcher.sendRequestAsByteArray", "response code: " + responseCode);
+ // All 2xx codes are successful.
+ if (responseCode / 100 == 2) {
+ is = conn.getInputStream();
+ } else {
+ is = conn.getErrorStream();
+ isError = true;
+ }
+
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ final byte[] buffer = new byte[1024];
+ int bytesRead;
+
+ while ((bytesRead = is.read(buffer)) != -1) {
+ baos.write(buffer, 0, bytesRead);
+ }
+
+ if (isError) {
+ handleBadResponse(url.toString(), baos.toByteArray());
+ if (responseCode == 401) {
+ throw new AuthException("Auth error");
+ }
+ return null;
+ }
+
+ byte[] response = baos.toByteArray();
+ LogUtil.i("HttpFetcher.sendRequestAsByteArray", "received " + response.length + " bytes");
+ long end = SystemClock.uptimeMillis();
+ LogUtil.i("HttpFetcher.sendRequestAsByteArray", "fetch took " + (end - start) + " ms");
+ return response;
+ } finally {
+ closeQuietly(is);
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Send a http request to the given url.
+ *
+ * @return The response body as a InputStream. Or {@literal null} if status code is not 2xx.
+ * @throws java.io.IOException when an error occurs.
+ */
+ public static InputStream sendRequestAsInputStream(
+ Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+ throws IOException, AuthException {
+ Objects.requireNonNull(urlString);
+
+ URL url = reWriteUrl(context, urlString);
+ if (url == null) {
+ return null;
+ }
+
+ HttpURLConnection httpUrlConnection = null;
+ boolean isSuccess = false;
+ try {
+ httpUrlConnection = (HttpURLConnection) url.openConnection();
+ setMethodAndHeaders(httpUrlConnection, requestMethod, headers);
+ int responseCode = httpUrlConnection.getResponseCode();
+ LogUtil.i("HttpFetcher.sendRequestAsInputStream", "response code: " + responseCode);
+
+ if (responseCode == 401) {
+ throw new AuthException("Auth error");
+ } else if (responseCode / 100 == 2) { // All 2xx codes are successful.
+ InputStream is = httpUrlConnection.getInputStream();
+ if (is != null) {
+ is = new HttpInputStreamWrapper(httpUrlConnection, is);
+ isSuccess = true;
+ return is;
+ }
+ }
+
+ return null;
+ } finally {
+ if (httpUrlConnection != null && !isSuccess) {
+ httpUrlConnection.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Set http method and headers.
+ *
+ * @param conn The connection to add headers to.
+ * @param requestMethod request method
+ * @param headers http headers where the first item in the pair is the key and second item is the
+ * value.
+ */
+ private static void setMethodAndHeaders(
+ HttpURLConnection conn, String requestMethod, List<Pair<String, String>> headers)
+ throws ProtocolException {
+ conn.setRequestMethod(requestMethod);
+ if (headers != null) {
+ for (Pair<String, String> pair : headers) {
+ conn.setRequestProperty(pair.first, pair.second);
+ }
+ }
+ }
+
+ private static String obfuscateUrl(String urlString) {
+ final Uri uri = Uri.parse(urlString);
+ final Builder builder =
+ new Builder().scheme(uri.getScheme()).authority(uri.getAuthority()).path(uri.getPath());
+ final Set<String> names = uri.getQueryParameterNames();
+ for (String name : names) {
+ if (PARAM_ACCESS_TOKEN.equals(name)) {
+ builder.appendQueryParameter(name, "token");
+ } else {
+ final String value = uri.getQueryParameter(name);
+ if (PARAM_ID.equals(name)) {
+ builder.appendQueryParameter(name, MoreStrings.toSafeString(value));
+ } else {
+ builder.appendQueryParameter(name, value);
+ }
+ }
+ }
+ return builder.toString();
+ }
+
+ /** Same as {@link #getRequestAsString(Context, String, String, List)} with null headers. */
+ public static String getRequestAsString(Context context, String urlString)
+ throws IOException, AuthException {
+ return getRequestAsString(context, urlString, "GET" /* Default to get. */, null);
+ }
+
+ /**
+ * Send a http request to the given url.
+ *
+ * @param context The android context.
+ * @param urlString The url to request.
+ * @param headers Http headers to pass in the request. {@literal null} is allowed.
+ * @return The response body as a String. Or {@literal null} if status code is not 2xx.
+ * @throws java.io.IOException when an error occurs.
+ */
+ public static String getRequestAsString(
+ Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+ throws IOException, AuthException {
+ final byte[] byteArr = sendRequestAsByteArray(context, urlString, requestMethod, headers);
+ if (byteArr == null) {
+ // Encountered error response... just return.
+ return null;
+ }
+ final String response = new String(byteArr);
+ LogUtil.i("HttpFetcher.getRequestAsString", "response body: " + response);
+ return response;
+ }
+
+ /**
+ * Lookup up url re-write rules from gServices and apply to the given url.
+ *
+ * <p>https://wiki.corp.google.com/twiki/bin/view/Main/AndroidGservices#URL_Rewriting_Rules
+ *
+ * @return The new url.
+ */
+ private static URL reWriteUrl(Context context, String url) {
+ final UrlRules rules = UrlRules.getRules(context.getContentResolver());
+ final UrlRules.Rule rule = rules.matchRule(url);
+ final String newUrl = rule.apply(url);
+
+ if (newUrl == null) {
+ if (LogUtil.isDebugEnabled()) {
+ // Url is blocked by re-write.
+ LogUtil.i(
+ "HttpFetcher.reWriteUrl",
+ "url " + obfuscateUrl(url) + " is blocked. Ignoring request.");
+ }
+ return null;
+ }
+
+ if (LogUtil.isDebugEnabled()) {
+ LogUtil.i("HttpFetcher.reWriteUrl", "fetching " + obfuscateUrl(newUrl));
+ if (!newUrl.equals(url)) {
+ LogUtil.i(
+ "HttpFetcher.reWriteUrl",
+ "Original url: " + obfuscateUrl(url) + ", after re-write: " + obfuscateUrl(newUrl));
+ }
+ }
+
+ URL urlObject = null;
+ try {
+ urlObject = new URL(newUrl);
+ } catch (MalformedURLException e) {
+ LogUtil.e("HttpFetcher.reWriteUrl", "failed to parse url: " + url, e);
+ }
+ return urlObject;
+ }
+
+ private static void handleBadResponse(String url, byte[] response) {
+ LogUtil.i("HttpFetcher.handleBadResponse", "Got bad response code from url: " + url);
+ LogUtil.i("HttpFetcher.handleBadResponse", new String(response));
+ }
+
+ /** Disconnect {@link HttpURLConnection} when InputStream is closed */
+ private static class HttpInputStreamWrapper extends FilterInputStream {
+
+ final HttpURLConnection mHttpUrlConnection;
+ final long mStartMillis = SystemClock.uptimeMillis();
+
+ public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) {
+ super(in);
+ mHttpUrlConnection = conn;
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ mHttpUrlConnection.disconnect();
+ if (LogUtil.isDebugEnabled()) {
+ long endMillis = SystemClock.uptimeMillis();
+ LogUtil.i("HttpFetcher.close", "fetch took " + (endMillis - mStartMillis) + " ms");
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationFragment.java b/java/com/android/incallui/calllocation/impl/LocationFragment.java
new file mode 100644
index 000000000..b152cd683
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationFragment.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.animation.LayoutTransition;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.baseui.BaseFragment;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Fragment which shows location during E911 calls, to supplement the user with accurate location
+ * information in case the user is asked for their location by the emergency responder.
+ *
+ * <p>If location data is inaccurate, stale, or unavailable, this should not be shown.
+ */
+public class LocationFragment extends BaseFragment<LocationPresenter, LocationPresenter.LocationUi>
+ implements LocationPresenter.LocationUi {
+
+ private static final String ADDRESS_DELIMITER = ",";
+
+ // Indexes used to animate fading between views
+ private static final int LOADING_VIEW_INDEX = 0;
+ private static final int LOCATION_VIEW_INDEX = 1;
+ private static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
+
+ private ViewAnimator viewAnimator;
+ private ImageView locationMap;
+ private TextView addressLine1;
+ private TextView addressLine2;
+ private TextView latLongLine;
+ private Location location;
+ private ViewGroup locationLayout;
+
+ private boolean isMapSet;
+ private boolean isAddressSet;
+ private boolean isLocationSet;
+ private boolean hasTimeoutStarted;
+
+ private final Handler handler = new Handler();
+ private final Runnable dataTimeoutRunnable =
+ () -> {
+ LogUtil.i(
+ "LocationFragment.dataTimeoutRunnable",
+ "timed out so animate any future layout changes");
+ locationLayout.setLayoutTransition(new LayoutTransition());
+ showLocationNow();
+ };
+
+ @Override
+ public LocationPresenter createPresenter() {
+ return new LocationPresenter();
+ }
+
+ @Override
+ public LocationPresenter.LocationUi getUi() {
+ return this;
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.location_fragment, container, false);
+ viewAnimator = (ViewAnimator) view.findViewById(R.id.location_view_animator);
+ locationMap = (ImageView) view.findViewById(R.id.location_map);
+ addressLine1 = (TextView) view.findViewById(R.id.address_line_one);
+ addressLine2 = (TextView) view.findViewById(R.id.address_line_two);
+ latLongLine = (TextView) view.findViewById(R.id.lat_long_line);
+ locationLayout = (ViewGroup) view.findViewById(R.id.location_layout);
+ view.setOnClickListener(
+ v -> {
+ LogUtil.enterBlock("LocationFragment.onCreateView");
+ launchMap();
+ });
+ return view;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ handler.removeCallbacks(dataTimeoutRunnable);
+ }
+
+ @Override
+ public void setMap(Drawable mapImage) {
+ LogUtil.enterBlock("LocationFragment.setMap");
+ isMapSet = true;
+ locationMap.setVisibility(View.VISIBLE);
+ locationMap.setImageDrawable(mapImage);
+ displayWhenReady();
+ }
+
+ @Override
+ public void setAddress(String address) {
+ LogUtil.i("LocationFragment.setAddress", address);
+ isAddressSet = true;
+ addressLine1.setVisibility(View.VISIBLE);
+ addressLine2.setVisibility(View.VISIBLE);
+ if (TextUtils.isEmpty(address)) {
+ addressLine1.setText(null);
+ addressLine2.setText(null);
+ } else {
+
+ // Split the address after the first delimiter for display, if present.
+ // For example, "1600 Amphitheatre Parkway, Mountain View, CA 94043"
+ // => "1600 Amphitheatre Parkway"
+ // => "Mountain View, CA 94043"
+ int splitIndex = address.indexOf(ADDRESS_DELIMITER);
+ if (splitIndex >= 0) {
+ updateText(addressLine1, address.substring(0, splitIndex).trim());
+ updateText(addressLine2, address.substring(splitIndex + 1).trim());
+ } else {
+ updateText(addressLine1, address);
+ updateText(addressLine2, null);
+ }
+ }
+ displayWhenReady();
+ }
+
+ @Override
+ public void setLocation(Location location) {
+ LogUtil.i("LocationFragment.setLocation", String.valueOf(location));
+ isLocationSet = true;
+ this.location = location;
+
+ if (location != null) {
+ latLongLine.setVisibility(View.VISIBLE);
+ latLongLine.setText(
+ getContext()
+ .getString(
+ R.string.lat_long_format, location.getLatitude(), location.getLongitude()));
+ }
+ displayWhenReady();
+ }
+
+ private void displayWhenReady() {
+ // Show the location if all data has loaded, otherwise prime the timeout
+ if (isMapSet && isAddressSet && isLocationSet) {
+ showLocationNow();
+ } else if (!hasTimeoutStarted) {
+ handler.postDelayed(dataTimeoutRunnable, TIMEOUT_MILLIS);
+ hasTimeoutStarted = true;
+ }
+ }
+
+ private void showLocationNow() {
+ handler.removeCallbacks(dataTimeoutRunnable);
+ if (viewAnimator.getDisplayedChild() != LOCATION_VIEW_INDEX) {
+ viewAnimator.setDisplayedChild(LOCATION_VIEW_INDEX);
+ }
+ }
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ private void launchMap() {
+ if (location != null) {
+ startActivity(
+ LocationUrlBuilder.getShowMapIntent(
+ location, addressLine1.getText(), addressLine2.getText()));
+ }
+ }
+
+ private static void updateText(TextView view, String text) {
+ if (!Objects.equals(text, view.getText())) {
+ view.setText(text);
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationHelper.java b/java/com/android/incallui/calllocation/impl/LocationHelper.java
new file mode 100644
index 000000000..645e9b86a
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationHelper.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.location.Location;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.MainThread;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
+import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.location.LocationListener;
+import com.google.android.gms.location.LocationRequest;
+import com.google.android.gms.location.LocationServices;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Uses the Fused location service to get location and pass updates on to listeners. */
+public class LocationHelper {
+
+ private static final int MIN_UPDATE_INTERVAL_MS = 30 * 1000;
+ private static final int LAST_UPDATE_THRESHOLD_MS = 60 * 1000;
+ private static final int LOCATION_ACCURACY_THRESHOLD_METERS = 100;
+
+ private final LocationHelperInternal locationHelperInternal;
+ private final List<LocationListener> listeners = new ArrayList<>();
+
+ @MainThread
+ LocationHelper(Context context) {
+ Assert.isMainThread();
+ Assert.checkArgument(canGetLocation(context));
+ locationHelperInternal = new LocationHelperInternal(context);
+ }
+
+ static boolean canGetLocation(Context context) {
+ if (!PermissionsUtil.hasLocationPermissions(context)) {
+ LogUtil.i("LocationHelper.canGetLocation", "no location permissions.");
+ return false;
+ }
+
+ // Ensure that both system location setting is on and google location services are enabled.
+ if (!GoogleLocationSettingHelper.isGoogleLocationServicesEnabled(context)
+ || !GoogleLocationSettingHelper.isSystemLocationSettingEnabled(context)) {
+ LogUtil.i("LocationHelper.canGetLocation", "location service is disabled.");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Whether the location is valid. We consider it valid if it was recorded within the specified
+ * time threshold of the present and has an accuracy less than the specified distance threshold.
+ *
+ * @param location The location to determine the validity of.
+ * @return {@code true} if the location is valid, and {@code false} otherwise.
+ */
+ static boolean isValidLocation(Location location) {
+ if (location != null) {
+ long locationTimeMs = location.getTime();
+ long elapsedTimeMs = System.currentTimeMillis() - locationTimeMs;
+ if (elapsedTimeMs > LAST_UPDATE_THRESHOLD_MS) {
+ LogUtil.i("LocationHelper.isValidLocation", "stale location, age: " + elapsedTimeMs);
+ return false;
+ }
+ if (location.getAccuracy() > LOCATION_ACCURACY_THRESHOLD_METERS) {
+ LogUtil.i("LocationHelper.isValidLocation", "poor accuracy: " + location.getAccuracy());
+ return false;
+ }
+ return true;
+ }
+ LogUtil.i("LocationHelper.isValidLocation", "no location");
+ return false;
+ }
+
+ @MainThread
+ void addLocationListener(LocationListener listener) {
+ Assert.isMainThread();
+ listeners.add(listener);
+ }
+
+ @MainThread
+ void removeLocationListener(LocationListener listener) {
+ Assert.isMainThread();
+ listeners.remove(listener);
+ }
+
+ @MainThread
+ void close() {
+ Assert.isMainThread();
+ LogUtil.enterBlock("LocationHelper.close");
+ listeners.clear();
+
+ if (locationHelperInternal != null) {
+ locationHelperInternal.close();
+ }
+ }
+
+ @MainThread
+ void onLocationChanged(Location location, boolean isConnected) {
+ Assert.isMainThread();
+ LogUtil.i("LocationHelper.onLocationChanged", "location: " + location);
+
+ for (LocationListener listener : listeners) {
+ listener.onLocationChanged(location);
+ }
+ }
+
+ /**
+ * This class contains all the asynchronous callbacks. It only posts location changes back to the
+ * outer class on the main thread.
+ */
+ private class LocationHelperInternal
+ implements ConnectionCallbacks, OnConnectionFailedListener, LocationListener {
+
+ private final GoogleApiClient apiClient;
+ private final ConnectivityManager connectivityManager;
+ private final Handler mainThreadHandler = new Handler();
+
+ @MainThread
+ LocationHelperInternal(Context context) {
+ Assert.isMainThread();
+ apiClient =
+ new GoogleApiClient.Builder(context)
+ .addApi(LocationServices.API)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+
+ LogUtil.i("LocationHelperInternal", "Connecting to location service...");
+ apiClient.connect();
+
+ connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ void close() {
+ if (apiClient.isConnected()) {
+ LogUtil.i("LocationHelperInternal", "disconnecting");
+ LocationServices.FusedLocationApi.removeLocationUpdates(apiClient, this);
+ apiClient.disconnect();
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle bundle) {
+ LogUtil.enterBlock("LocationHelperInternal.onConnected");
+ LocationRequest locationRequest =
+ LocationRequest.create()
+ .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)
+ .setInterval(MIN_UPDATE_INTERVAL_MS)
+ .setFastestInterval(MIN_UPDATE_INTERVAL_MS);
+
+ LocationServices.FusedLocationApi.requestLocationUpdates(apiClient, locationRequest, this)
+ .setResultCallback(
+ new ResultCallback<Status>() {
+ @Override
+ public void onResult(Status status) {
+ if (status.getStatus().isSuccess()) {
+ onLocationChanged(LocationServices.FusedLocationApi.getLastLocation(apiClient));
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ // Post new location on main thread
+ mainThreadHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ LocationHelper.this.onLocationChanged(location, isConnected());
+ }
+ });
+ }
+
+ /** @return Whether the phone is connected to data. */
+ private boolean isConnected() {
+ if (connectivityManager == null) {
+ return false;
+ }
+ NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnectedOrConnecting();
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationPresenter.java b/java/com/android/incallui/calllocation/impl/LocationPresenter.java
new file mode 100644
index 000000000..a56fd3b3c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationPresenter.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.baseui.Presenter;
+import com.android.incallui.baseui.Ui;
+import com.google.android.gms.location.LocationListener;
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+
+/**
+ * Presenter for the {@code LocationFragment}.
+ *
+ * <p>Performs lookup for the address and map image to show.
+ */
+public class LocationPresenter extends Presenter<LocationPresenter.LocationUi>
+ implements LocationListener {
+
+ private Location mLastLocation;
+ private AsyncTask mDownloadMapTask;
+ private AsyncTask mReverseGeocodeTask;
+
+ LocationPresenter() {}
+
+ @Override
+ public void onUiReady(LocationUi ui) {
+ LogUtil.i("LocationPresenter.onUiReady", "");
+ super.onUiReady(ui);
+ updateLocation(mLastLocation, true);
+ }
+
+ @Override
+ public void onUiUnready(LocationUi ui) {
+ LogUtil.i("LocationPresenter.onUiUnready", "");
+ super.onUiUnready(ui);
+
+ if (mDownloadMapTask != null) {
+ mDownloadMapTask.cancel(true);
+ }
+ if (mReverseGeocodeTask != null) {
+ mReverseGeocodeTask.cancel(true);
+ }
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ LogUtil.i("LocationPresenter.onLocationChanged", "");
+ updateLocation(location, false);
+ }
+
+ private void updateLocation(Location location, boolean forceUpdate) {
+ LogUtil.i("LocationPresenter.updateLocation", "location: " + location);
+ if (forceUpdate || !Objects.equals(mLastLocation, location)) {
+ mLastLocation = location;
+ if (LocationHelper.isValidLocation(location)) {
+ LocationUi ui = getUi();
+ mDownloadMapTask = new DownloadMapImageTask(new WeakReference<>(ui)).execute(location);
+ mReverseGeocodeTask = new ReverseGeocodeTask(new WeakReference<>(ui)).execute(location);
+ if (ui != null) {
+ ui.setLocation(location);
+ } else {
+ LogUtil.i("LocationPresenter.updateLocation", "no Ui");
+ }
+ }
+ }
+ }
+
+ /** UI interface */
+ public interface LocationUi extends Ui {
+
+ void setAddress(String address);
+
+ void setMap(Drawable mapImage);
+
+ void setLocation(Location location);
+
+ Context getContext();
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java
new file mode 100644
index 000000000..a57bdf613
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.location.Location;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import java.util.Locale;
+
+class LocationUrlBuilder {
+
+ // Static Map API path constants.
+ private static final String HTTPS_SCHEME = "https";
+ private static final String MAPS_API_DOMAIN = "maps.googleapis.com";
+ private static final String MAPS_PATH = "maps";
+ private static final String API_PATH = "api";
+ private static final String STATIC_MAP_PATH = "staticmap";
+ private static final String GEOCODE_PATH = "geocode";
+ private static final String GEOCODE_OUTPUT_TYPE = "json";
+
+ // Static Map API parameter constants.
+ private static final String KEY_PARAM_KEY = "key";
+ private static final String CENTER_PARAM_KEY = "center";
+ private static final String ZOOM_PARAM_KEY = "zoom";
+ private static final String SCALE_PARAM_KEY = "scale";
+ private static final String SIZE_PARAM_KEY = "size";
+ private static final String MARKERS_PARAM_KEY = "markers";
+
+ private static final String ZOOM_PARAM_VALUE = Integer.toString(16);
+
+ private static final String LAT_LONG_DELIMITER = ",";
+
+ private static final String MARKER_DELIMITER = "|";
+ private static final String MARKER_STYLE_DELIMITER = ":";
+ private static final String MARKER_STYLE_COLOR = "color";
+ private static final String MARKER_STYLE_COLOR_RED = "red";
+
+ private static final String LAT_LNG_PARAM_KEY = "latlng";
+
+ private static final String ANDROID_API_KEY_VALUE = "AIzaSyAXdDnif6B7sBYxU8hzw9qAp3pRPVHs060";
+ private static final String BROWSER_API_KEY_VALUE = "AIzaSyBfLlvWYndiQ3RFEHli65qGQH36QIxdyCI";
+
+ /**
+ * Generates the URL to a static map image for the given location.
+ *
+ * <p>This image has the following characteristics:
+ *
+ * <p>- It is centered at the given latitude and longitutde. - It is scaled according to the
+ * device's pixel density. - There is a red marker at the given latitude and longitude.
+ *
+ * <p>Source: https://developers.google.com/maps/documentation/staticmaps/
+ *
+ * @param contxt The context.
+ * @param Location A location.
+ * @return The URL of a static map image url of the given location.
+ */
+ public static String getStaticMapUrl(Context context, Location location) {
+ final Uri.Builder builder = new Uri.Builder();
+ Resources res = context.getResources();
+ String size =
+ res.getDimensionPixelSize(R.dimen.location_map_width)
+ + "x"
+ + res.getDimensionPixelSize(R.dimen.location_map_height);
+
+ builder
+ .scheme(HTTPS_SCHEME)
+ .authority(MAPS_API_DOMAIN)
+ .appendPath(MAPS_PATH)
+ .appendPath(API_PATH)
+ .appendPath(STATIC_MAP_PATH)
+ .appendQueryParameter(CENTER_PARAM_KEY, getFormattedLatLng(location))
+ .appendQueryParameter(ZOOM_PARAM_KEY, ZOOM_PARAM_VALUE)
+ .appendQueryParameter(SIZE_PARAM_KEY, size)
+ .appendQueryParameter(SCALE_PARAM_KEY, Float.toString(res.getDisplayMetrics().density))
+ .appendQueryParameter(MARKERS_PARAM_KEY, getMarkerUrlParamValue(location))
+ .appendQueryParameter(KEY_PARAM_KEY, ANDROID_API_KEY_VALUE);
+
+ return builder.build().toString();
+ }
+
+ /**
+ * Generates the URL for a request to reverse geocode the given location.
+ *
+ * <p>Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
+ *
+ * @param Location A location.
+ */
+ public static String getReverseGeocodeUrl(Location location) {
+ final Uri.Builder builder = new Uri.Builder();
+
+ builder
+ .scheme(HTTPS_SCHEME)
+ .authority(MAPS_API_DOMAIN)
+ .appendPath(MAPS_PATH)
+ .appendPath(API_PATH)
+ .appendPath(GEOCODE_PATH)
+ .appendPath(GEOCODE_OUTPUT_TYPE)
+ .appendQueryParameter(LAT_LNG_PARAM_KEY, getFormattedLatLng(location))
+ .appendQueryParameter(KEY_PARAM_KEY, BROWSER_API_KEY_VALUE);
+
+ return builder.build().toString();
+ }
+
+ public static Intent getShowMapIntent(
+ Location location, @Nullable CharSequence addressLine1, @Nullable CharSequence addressLine2) {
+
+ String latLong = getFormattedLatLng(location);
+ String url = String.format(Locale.US, "geo: %s?q=%s", latLong, latLong);
+
+ // Add a map label
+ if (addressLine1 != null) {
+ if (addressLine2 != null) {
+ url +=
+ String.format(Locale.US, "(%s, %s)", addressLine1.toString(), addressLine2.toString());
+ } else {
+ url += String.format(Locale.US, "(%s)", addressLine1.toString());
+ }
+ } else {
+ // TODO: i18n
+ url +=
+ String.format(
+ Locale.US,
+ "(Latitude: %f, Longitude: %f)",
+ location.getLatitude(),
+ location.getLongitude());
+ }
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ intent.setPackage("com.google.android.apps.maps");
+ return intent;
+ }
+
+ /**
+ * Returns a comma-separated latitude and longitude pair, formatted for use as a URL parameter
+ * value.
+ *
+ * @param location A location.
+ * @return The comma-separated latitude and longitude pair of that location.
+ */
+ @VisibleForTesting
+ static String getFormattedLatLng(Location location) {
+ return location.getLatitude() + LAT_LONG_DELIMITER + location.getLongitude();
+ }
+
+ /**
+ * Returns the URL parameter value for the marker, specifying its style and position.
+ *
+ * @param location A location.
+ * @return The URL parameter value for the marker.
+ */
+ @VisibleForTesting
+ static String getMarkerUrlParamValue(Location location) {
+ return MARKER_STYLE_COLOR
+ + MARKER_STYLE_DELIMITER
+ + MARKER_STYLE_COLOR_RED
+ + MARKER_DELIMITER
+ + getFormattedLatLng(location);
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java
new file mode 100644
index 000000000..eb5957b05
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.location.Location;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi;
+import java.lang.ref.WeakReference;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+class ReverseGeocodeTask extends AsyncTask<Location, Void, String> {
+
+ // Below are the JSON keys for the reverse geocode response.
+ // Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
+ private static final String JSON_KEY_RESULTS = "results";
+ private static final String JSON_KEY_ADDRESS = "formatted_address";
+ private static final String JSON_KEY_ADDRESS_COMPONENTS = "address_components";
+ private static final String JSON_KEY_PREMISE = "premise";
+ private static final String JSON_KEY_TYPES = "types";
+ private static final String JSON_KEY_LONG_NAME = "long_name";
+ private static final String JSON_KEY_SHORT_NAME = "short_name";
+
+ private WeakReference<LocationUi> mUiReference;
+
+ public ReverseGeocodeTask(WeakReference<LocationUi> uiReference) {
+ mUiReference = uiReference;
+ }
+
+ @Override
+ protected String doInBackground(Location... locations) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return null;
+ }
+ if (locations == null || locations.length == 0) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "No location provided");
+ return null;
+ }
+
+ try {
+ String address = null;
+ String url = LocationUrlBuilder.getReverseGeocodeUrl(locations[0]);
+
+ TrafficStats.setThreadStatsTag(TrafficStatsTags.REVERSE_GEOCODE_TAG);
+ String jsonResponse = HttpFetcher.getRequestAsString(ui.getContext(), url);
+
+ // Parse the JSON response for the formatted address of the first result.
+ JSONObject responseObject = new JSONObject(jsonResponse);
+ if (responseObject != null) {
+ JSONArray results = responseObject.optJSONArray(JSON_KEY_RESULTS);
+ if (results != null && results.length() > 0) {
+ JSONObject topResult = results.optJSONObject(0);
+ if (topResult != null) {
+ address = topResult.getString(JSON_KEY_ADDRESS);
+
+ // Strip off the Premise component from the address, if present.
+ JSONArray components = topResult.optJSONArray(JSON_KEY_ADDRESS_COMPONENTS);
+ if (components != null) {
+ boolean stripped = false;
+ for (int i = 0; !stripped && i < components.length(); i++) {
+ JSONObject component = components.optJSONObject(i);
+ JSONArray types = component.optJSONArray(JSON_KEY_TYPES);
+ if (types != null) {
+ for (int j = 0; !stripped && j < types.length(); j++) {
+ if (JSON_KEY_PREMISE.equals(types.getString(j))) {
+ String premise = null;
+ if (component.has(JSON_KEY_SHORT_NAME)
+ && address.startsWith(component.getString(JSON_KEY_SHORT_NAME))) {
+ premise = component.getString(JSON_KEY_SHORT_NAME);
+ } else if (component.has(JSON_KEY_LONG_NAME)
+ && address.startsWith(component.getString(JSON_KEY_LONG_NAME))) {
+ premise = component.getString(JSON_KEY_SHORT_NAME);
+ }
+ if (premise != null) {
+ int index = address.indexOf(',', premise.length());
+ if (index > 0 && index < address.length()) {
+ address = address.substring(index + 1).trim();
+ }
+ stripped = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Strip off the country, if its USA. Note: unfortunately the country in the formatted
+ // address field doesn't match the country in the address component fields (USA != US)
+ // so we can't easily strip off the country for all cases, thus this hack.
+ if (address.endsWith(", USA")) {
+ address = address.substring(0, address.length() - 5);
+ }
+ }
+ }
+ }
+
+ return address;
+ } catch (AuthException ex) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "AuthException", ex);
+ return null;
+ } catch (JSONException ex) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "JSONException", ex);
+ return null;
+ } catch (Exception ex) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "Exception!!!", ex);
+ return null;
+ } finally {
+ TrafficStats.clearThreadStatsTag();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(String address) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return;
+ }
+
+ try {
+ ui.setAddress(address);
+ } catch (Exception ex) {
+ LogUtil.e("ReverseGeocodeTask.onPostExecute", "Exception!!!", ex);
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
new file mode 100644
index 000000000..02cc2e083
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+/** Constants used for logging */
+public class TrafficStatsTags {
+
+ /**
+ * Must be greater than {@link com.android.contacts.common.util.TrafficStatsTags#TAG_MAX}, to
+ * respect the namespace of the tags in ContactsCommon.
+ */
+ public static final int DOWNLOAD_LOCATION_MAP_TAG = 0xd000;
+
+ public static final int REVERSE_GEOCODE_TAG = 0xd001;
+}
diff --git a/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml
new file mode 100644
index 000000000..a6bd07542
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+~ Copyright (C) 2015 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License
+-->
+
+<ViewAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/location_view_animator"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:background="@android:color/white"
+ android:elevation="2dp"
+ android:inAnimation="@android:anim/fade_in"
+ android:measureAllChildren="true"
+ android:outAnimation="@android:anim/fade_out">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/location_loading_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical">
+
+ <ProgressBar
+ android:id="@+id/location_loading_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="28dp"
+ android:layout_marginBottom="12dp"
+ android:layout_gravity="center_horizontal"/>
+
+ <TextView
+ android:id="@+id/location_loading_text"
+ style="@style/LocationLoadingTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="24sp"
+ android:layout_marginBottom="20dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"
+ android:gravity="center"
+ android:text="@string/location_loading"/>
+
+ </LinearLayout>
+
+ <GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/location_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:columnCount="2"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/location_address_title"
+ style="@style/LocationAddressTitleTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="20sp"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:text="@string/location_title"/>
+
+ <ImageView
+ android:id="@+id/location_map"
+ android:layout_width="@dimen/location_map_width"
+ android:layout_height="@dimen/location_map_height"
+ android:layout_margin="16dp"
+ android:layout_gravity="end|center_vertical"
+ android:layout_rowSpan="4"
+ android:contentDescription="@string/location_map_description"
+ android:scaleType="centerCrop"
+ android:visibility="invisible"
+ tools:src="?android:colorPrimaryDark"
+ tools:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/address_line_one"
+ style="@style/LocationAddressTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="24sp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:visibility="invisible"
+ tools:text="1600 Amphitheatre Pkwy And a bit"
+ tools:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/address_line_two"
+ style="@style/LocationAddressTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="24sp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:visibility="invisible"
+ tools:text="Mountain View, CA 94043"
+ tools:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/lat_long_line"
+ style="@style/LocationLatLongTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="24sp"
+ android:layout_marginBottom="12dp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:visibility="invisible"
+ tools:text="Lat: 37.421719, Long: -122.085297"
+ tools:visibility="visible"/>
+
+ </GridLayout>
+
+</ViewAnimator>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/dimens.xml b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml
new file mode 100644
index 000000000..1f4181607
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 Google Inc. All Rights Reserved. -->
+<resources>
+ <dimen name="location_map_width">92dp</dimen>
+ <dimen name="location_map_height">92dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/strings.xml b/java/com/android/incallui/calllocation/impl/res/values/strings.xml
new file mode 100644
index 000000000..ef7c1624c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Description for location map shown during emergency calls. [CHAR LIMIT=NONE] -->
+ <string name="location_map_description">Emergency Location Map</string>
+
+ <!-- Label for the address and map shown during emergency calls. [CHAR LIMIT=20] -->
+ <string name="location_title">You are here</string>
+
+ <string name="lat_long_format"><xliff:g id="latitude">%f</xliff:g>, <xliff:g id="longitude">%f</xliff:g></string>
+
+ <!-- Progress indicator loading text. [CHAR LIMIT=20] -->
+ <string name="location_loading">Finding your location</string>
+
+</resources>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/styles.xml b/java/com/android/incallui/calllocation/impl/res/values/styles.xml
new file mode 100644
index 000000000..866a4edb6
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2015 Google Inc. All Rights Reserved. -->
+<resources>
+
+ <style name="LocationAddressTitleTextStyle">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#dd000000</item>
+ <item name="android:fontFamily">sans-serif-medium</item>
+ </style>
+
+ <style name="LocationAddressTextStyle">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">#dd000000</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+
+ <style name="LocationLatLongTextStyle">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#88000000</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+
+ <style name="LocationLoadingTextStyle">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#dd000000</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+</resources>