Added location permission request inline the search fragment.

This change adds a prompt inline the search fragment list view
to request the location permission (if they haven't already
granted it).

screenshot: http://screen/upu9t55mghq
Bug: 65858857
Test: NSFT, SAT, SCMT
PiperOrigin-RevId: 169447095
Change-Id: I6c312057ff3c4e2362ce21b0c57e1e5de7b25ce0
diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
index 3cf8fb5..1dbd953 100644
--- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
+++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
@@ -16,6 +16,8 @@
 
 package com.android.dialer.searchfragment.list;
 
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+
 import android.app.Fragment;
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.Loader;
@@ -76,6 +78,7 @@
   private static final String KEY_SHOW_ZERO_SUGGEST = "use_zero_suggest";
 
   @VisibleForTesting public static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
+  @VisibleForTesting private static final int LOCATION_PERMISSION_REQUEST_CODE = 2;
 
   private static final int CONTACTS_LOADER_ID = 0;
   private static final int NEARBY_PLACES_LOADER_ID = 1;
@@ -279,6 +282,12 @@
         emptyContentView.setVisibility(View.GONE);
         initLoaders();
       }
+    } else if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
+      if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+        // Force a refresh of the data since we were missing the permission before this.
+        loadNearbyPlacesCursor();
+        adapter.hideLocationPermissionRequest();
+      }
     }
   }
 
@@ -317,6 +326,12 @@
 
   // Should not be called before remote directories (not contacts) have finished loading.
   private void loadNearbyPlacesCursor() {
+    if (!PermissionsUtil.hasLocationPermissions(getContext())) {
+      if (adapter != null) {
+        adapter.showLocationPermissionRequest(v -> requestLocationPermission());
+      }
+      return;
+    }
     // Cancel existing load if one exists.
     ThreadUtil.getUiThreadHandler().removeCallbacks(loadNearbyPlacesRunnable);
 
@@ -328,6 +343,16 @@
         .postDelayed(loadNearbyPlacesRunnable, NETWORK_SEARCH_DELAY_MILLIS);
   }
 
+  private void requestLocationPermission() {
+    Assert.checkArgument(
+        !PermissionsUtil.hasPermission(getContext(), ACCESS_FINE_LOCATION),
+        "attempted to request already granted location permission");
+    String[] deniedPermissions =
+        PermissionsUtil.getPermissionsCurrentlyDenied(
+            getContext(), PermissionsUtil.allLocationGroupPermissionsUsedInDialer);
+    requestPermissions(deniedPermissions, LOCATION_PERMISSION_REQUEST_CODE);
+  }
+
   @Override
   public void onResume() {
     super.onResume();
diff --git a/java/com/android/dialer/searchfragment/list/SearchAdapter.java b/java/com/android/dialer/searchfragment/list/SearchAdapter.java
index d4b5cf2..358a59a 100644
--- a/java/com/android/dialer/searchfragment/list/SearchAdapter.java
+++ b/java/com/android/dialer/searchfragment/list/SearchAdapter.java
@@ -23,6 +23,8 @@
 import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import com.android.dialer.callcomposer.CallComposerActivity;
 import com.android.dialer.callintent.CallInitiationType;
@@ -54,6 +56,7 @@
   private boolean showZeroSuggest;
   private String query;
   private CallInitiationType.Type callInitiationType = CallInitiationType.Type.UNKNOWN_INITIATION;
+  private OnClickListener locationRequestClickListener;
 
   @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
   public SearchAdapter(Activity activity, SearchCursorManager searchCursorManager) {
@@ -81,6 +84,10 @@
       case RowType.SEARCH_ACTION:
         return new SearchActionViewHolder(
             LayoutInflater.from(activity).inflate(R.layout.search_action_layout, root, false));
+      case RowType.LOCATION_REQUEST:
+        return new LocationPermissionViewHolder(
+            LayoutInflater.from(activity).inflate(R.layout.location_permission_row, root, false),
+            locationRequestClickListener);
       case RowType.INVALID:
       default:
         throw Assert.createIllegalStateFailException("Invalid RowType: " + rowType);
@@ -107,6 +114,8 @@
     } else if (holder instanceof SearchActionViewHolder) {
       ((SearchActionViewHolder) holder)
           .setAction(searchCursorManager.getSearchAction(position), position, query);
+    } else if (holder instanceof LocationPermissionViewHolder) {
+      // No-op
     } else {
       throw Assert.createIllegalStateFailException("Invalid ViewHolder: " + holder);
     }
@@ -165,6 +174,28 @@
     }
   }
 
+  /**
+   * Updates the adapter to show the location request row element. If the element was previously
+   * hidden, the adapter will call {@link #notifyDataSetChanged()}.
+   */
+  public void showLocationPermissionRequest(OnClickListener clickListener) {
+    Assert.isNotNull(locationRequestClickListener = clickListener);
+    if (searchCursorManager.showLocationPermissionRequest(true)) {
+      notifyDataSetChanged();
+    }
+  }
+
+  /**
+   * Updates the adapter to hide the location request row element. If the element was previously
+   * visible, the adapter will call {@link #notifyDataSetChanged()}.
+   */
+  void hideLocationPermissionRequest() {
+    locationRequestClickListener = null;
+    if (searchCursorManager.showLocationPermissionRequest(false)) {
+      notifyDataSetChanged();
+    }
+  }
+
   public void setRemoteContactsCursor(SearchCursor remoteContactsCursor) {
     if (searchCursorManager.setCorpDirectoryCursor(remoteContactsCursor)) {
       notifyDataSetChanged();
@@ -207,4 +238,17 @@
     Intent intent = CallComposerActivity.newIntent(activity, contact);
     DialerUtils.startActivityWithErrorToast(activity, intent);
   }
+
+  /** Viewholder for R.layout.location_permission_row that requests the location permission. */
+  private static class LocationPermissionViewHolder extends RecyclerView.ViewHolder {
+
+    LocationPermissionViewHolder(View itemView, OnClickListener locationRequestClickListener) {
+      super(itemView);
+      Assert.isNotNull(locationRequestClickListener);
+      itemView
+          .findViewById(
+              com.android.dialer.searchfragment.nearbyplaces.R.id.location_permission_allow)
+          .setOnClickListener(locationRequestClickListener);
+    }
+  }
 }
diff --git a/java/com/android/dialer/searchfragment/list/SearchCursorManager.java b/java/com/android/dialer/searchfragment/list/SearchCursorManager.java
index 3704e81..f8d1e1b 100644
--- a/java/com/android/dialer/searchfragment/list/SearchCursorManager.java
+++ b/java/com/android/dialer/searchfragment/list/SearchCursorManager.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer.searchfragment.list;
 
+import android.database.MatrixCursor;
 import android.support.annotation.IntDef;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
@@ -60,7 +61,8 @@
     SearchCursorManager.RowType.NEARBY_PLACES_ROW,
     SearchCursorManager.RowType.DIRECTORY_HEADER,
     SearchCursorManager.RowType.DIRECTORY_ROW,
-    SearchCursorManager.RowType.SEARCH_ACTION
+    SearchCursorManager.RowType.SEARCH_ACTION,
+    SearchCursorManager.RowType.LOCATION_REQUEST
   })
   @interface RowType {
     int INVALID = 0;
@@ -79,13 +81,20 @@
     int DIRECTORY_ROW = 6;
     /** A row containing a search action */
     int SEARCH_ACTION = 7;
+    /** A row which requests location permission */
+    int LOCATION_REQUEST = 8;
   }
 
+  private static final LocationPermissionCursor LOCATION_PERMISSION_CURSOR =
+      new LocationPermissionCursor(new String[0]);
+
   private SearchCursor contactsCursor = null;
   private SearchCursor nearbyPlacesCursor = null;
   private SearchCursor corpDirectoryCursor = null;
   private List<Integer> searchActions = new ArrayList<>();
 
+  private boolean showLocationPermissionRequest;
+
   /** Returns true if the cursor changed. */
   boolean setContactsCursor(@Nullable SearchCursor cursor) {
     if (cursor == contactsCursor) {
@@ -122,6 +131,15 @@
     return true;
   }
 
+  /** Returns true if the value changed. */
+  boolean showLocationPermissionRequest(boolean enabled) {
+    if (showLocationPermissionRequest == enabled) {
+      return false;
+    }
+    showLocationPermissionRequest = enabled;
+    return true;
+  }
+
   /** Returns true if a cursor changed. */
   boolean setCorpDirectoryCursor(@Nullable SearchCursor cursor) {
     if (cursor == corpDirectoryCursor) {
@@ -177,7 +195,9 @@
       count += contactsCursor.getCount();
     }
 
-    if (nearbyPlacesCursor != null) {
+    if (showLocationPermissionRequest) {
+      count++;
+    } else if (nearbyPlacesCursor != null) {
       count += nearbyPlacesCursor.getCount();
     }
 
@@ -203,6 +223,10 @@
       return cursor.isHeader() ? RowType.CONTACT_HEADER : RowType.CONTACT_ROW;
     }
 
+    if (cursor == LOCATION_PERMISSION_CURSOR) {
+      return RowType.LOCATION_REQUEST;
+    }
+
     if (cursor == nearbyPlacesCursor) {
       return cursor.isHeader() ? RowType.NEARBY_PLACES_HEADER : RowType.NEARBY_PLACES_ROW;
     }
@@ -214,9 +238,7 @@
   }
 
   /**
-   * Gets cursor corresponding to position in coalesced list of search cursors. Callers should
-   * ensure that {@link #getRowType(int)} doesn't correspond to header position, otherwise an
-   * exception will be thrown.
+   * Gets cursor corresponding to position in coalesced list of search cursors.
    *
    * @param position in coalesced list of search cursors
    * @return Cursor moved to position specific to passed in position.
@@ -232,7 +254,13 @@
       position -= count;
     }
 
-    if (nearbyPlacesCursor != null) {
+    if (showLocationPermissionRequest) {
+      if (position == 0) {
+        return LOCATION_PERMISSION_CURSOR;
+      }
+      position--;
+
+    } else if (nearbyPlacesCursor != null) {
       int count = nearbyPlacesCursor.getCount();
 
       if (position - count < 0) {
@@ -272,4 +300,30 @@
       corpDirectoryCursor = null;
     }
   }
+
+  /**
+   * No-op implementation of {@link android.database.Cursor} and {@link SearchCursor} for
+   * representing location permission request row elements.
+   */
+  private static class LocationPermissionCursor extends MatrixCursor implements SearchCursor {
+
+    LocationPermissionCursor(String[] columnNames) {
+      super(columnNames);
+    }
+
+    @Override
+    public boolean isHeader() {
+      return false;
+    }
+
+    @Override
+    public boolean updateQuery(String query) {
+      return false;
+    }
+
+    @Override
+    public long getDirectoryId() {
+      return 0;
+    }
+  }
 }
diff --git a/java/com/android/dialer/searchfragment/nearbyplaces/res/layout/location_permission_row.xml b/java/com/android/dialer/searchfragment/nearbyplaces/res/layout/location_permission_row.xml
new file mode 100644
index 0000000..800bf62
--- /dev/null
+++ b/java/com/android/dialer/searchfragment/nearbyplaces/res/layout/location_permission_row.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingStart="16dp"
+    android:paddingEnd="16dp"
+    android:paddingTop="16dp">
+
+  <ImageView
+      android:id="@+id/permission_image"
+      android:layout_width="56dp"
+      android:layout_height="56dp"
+      android:layout_marginEnd="16dp"
+      android:src="@drawable/quantum_ic_my_location_vd_theme_24"
+      android:tint="@color/dialer_secondary_text_color"/>
+
+  <TextView
+      android:id="@+id/permission_text"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_toEndOf="@id/permission_image"
+      android:minHeight="56dp"
+      android:text="@string/permission_no_location_for_search"
+      android:textSize="16sp"
+      android:textColor="@color/dialer_secondary_text_color"/>
+
+  <Button
+      android:id="@+id/location_permission_allow"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_alignParentEnd="true"
+      android:layout_below="@id/permission_text"
+      android:text="@string/nearby_places_allow"
+      android:textColor="@color/dialer_theme_color"
+      style="@style/Widget.AppCompat.Button.Borderless"/>
+</RelativeLayout>
\ No newline at end of file