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 package com.android.car.carlauncher; 17 18 import android.app.Activity; 19 import android.support.annotation.NonNull; 20 import android.support.v7.widget.RecyclerView; 21 import android.view.View; 22 import android.view.ViewGroup; 23 import android.widget.Filter; 24 import android.widget.Filterable; 25 import android.widget.ImageView; 26 import android.widget.TextView; 27 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.List; 31 import java.util.regex.Pattern; 32 33 /** 34 * The search result adapter that binds the filtered result to a list view. 35 */ 36 37 final class SearchResultAdapter 38 extends RecyclerView.Adapter<SearchResultAdapter.ViewHolder> implements Filterable { 39 private final Activity mContext; 40 private List<AppMetaData> mAllApps; 41 private List<AppMetaData> mSearchResults; 42 private AppSearchFilter filter; 43 private boolean mIsDistractionOptimizationRequired; 44 SearchResultAdapter(Activity context)45 public SearchResultAdapter(Activity context) { 46 mContext = context; 47 mSearchResults = null; 48 } 49 setIsDistractionOptimizationRequired(boolean isDistractionOptimizationRequired)50 public void setIsDistractionOptimizationRequired(boolean isDistractionOptimizationRequired) { 51 mIsDistractionOptimizationRequired = isDistractionOptimizationRequired; 52 notifyDataSetChanged(); 53 } 54 setAllApps(List<AppMetaData> apps)55 public void setAllApps(List<AppMetaData> apps) { 56 mAllApps = apps; 57 notifyDataSetChanged(); 58 } 59 60 public static class ViewHolder extends RecyclerView.ViewHolder { 61 private View mAppItem; 62 private ImageView mAppIconView; 63 private TextView mAppNameView; 64 ViewHolder(View view)65 public ViewHolder(View view) { 66 super(view); 67 mAppItem = view.findViewById(R.id.app_item); 68 mAppIconView = mAppItem.findViewById(R.id.app_icon); 69 mAppNameView = mAppItem.findViewById(R.id.app_name); 70 } 71 bind(AppMetaData app, View.OnClickListener listener)72 public void bind(AppMetaData app, View.OnClickListener listener) { 73 if (app == null) { 74 // Empty out the view 75 mAppIconView.setImageDrawable(null); 76 mAppNameView.setText(null); 77 mAppItem.setClickable(false); 78 mAppItem.setOnClickListener(null); 79 } else { 80 mAppIconView.setImageDrawable(app.getIcon()); 81 mAppNameView.setText(app.getDisplayName()); 82 mAppItem.setClickable(true); 83 mAppItem.setOnClickListener(listener); 84 } 85 } 86 } 87 88 /** 89 * This filter does a simple case insensitive text match based on the application 90 * display name. e.g. "gm" -> "gmail". 91 * It only matches apps starting with the search query. It does not do sub-string matching. 92 */ 93 private class AppSearchFilter extends Filter { 94 95 @Override performFiltering(CharSequence constraint)96 protected FilterResults performFiltering(CharSequence constraint) { 97 FilterResults results = new FilterResults(); 98 if (constraint != null && constraint.length() > 0) { 99 List filterList = new ArrayList(); 100 for (int i = 0; i < mAllApps.size(); i++) { 101 AppMetaData app = mAllApps.get(i); 102 if (shouldShowApp( 103 app.getDisplayName(), 104 constraint.toString(), 105 app.getIsDistractionOptimized())) { 106 filterList.add(app); 107 } 108 } 109 Collections.sort(filterList, AppLauncherUtils.ALPHABETICAL_COMPARATOR); 110 results.count = filterList.size(); 111 results.values = filterList; 112 } else { 113 results.count = 0; 114 results.values = null; 115 } 116 117 return results; 118 } 119 shouldShowApp( String displayName, String constraint, boolean isDistractionOptimized)120 private boolean shouldShowApp( 121 String displayName, String constraint, boolean isDistractionOptimized) { 122 if (!mIsDistractionOptimizationRequired 123 || (mIsDistractionOptimizationRequired && isDistractionOptimized)) { 124 Pattern pattern = Pattern.compile( 125 "^" + constraint + ".*$", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); 126 return pattern.matcher(displayName).matches(); 127 } 128 return false; 129 } 130 131 @Override publishResults(CharSequence constraint, FilterResults results)132 protected void publishResults(CharSequence constraint, FilterResults results) { 133 mSearchResults = (ArrayList) results.values; 134 notifyDataSetChanged(); 135 } 136 } 137 138 @Override getFilter()139 public Filter getFilter() { 140 if (filter == null) { 141 filter = new AppSearchFilter(); 142 } 143 return filter; 144 } 145 146 @NonNull 147 @Override onCreateViewHolder( @onNull ViewGroup parent, int viewType)148 public SearchResultAdapter.ViewHolder onCreateViewHolder( 149 @NonNull ViewGroup parent, int viewType) { 150 View view = mContext.getLayoutInflater().inflate(R.layout.app_search_result_item, null); 151 return new ViewHolder(view); 152 } 153 154 @Override onBindViewHolder(@onNull SearchResultAdapter.ViewHolder holder, int position)155 public void onBindViewHolder(@NonNull SearchResultAdapter.ViewHolder holder, int position) { 156 // Get the data item for this position 157 AppMetaData app = mSearchResults.get(position); 158 holder.bind(app, v -> AppLauncherUtils.launchApp(mContext, app)); 159 } 160 161 @Override getItemId(int position)162 public long getItemId(int position) { 163 return position; 164 } 165 166 @Override getItemCount()167 public int getItemCount() { 168 if (mSearchResults != null) { 169 return mSearchResults.size(); 170 } else { 171 return 0; 172 } 173 } 174 clearResults()175 void clearResults() { 176 mSearchResults.clear(); 177 notifyDataSetChanged(); 178 } 179 } 180