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