1 /* 2 * Copyright (C) 2015 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.launcher3.allapps; 17 18 import android.content.Context; 19 import android.content.Intent; 20 import android.content.pm.PackageManager; 21 import android.content.pm.ResolveInfo; 22 import android.content.res.Resources; 23 import android.graphics.Canvas; 24 import android.graphics.Paint; 25 import android.graphics.PointF; 26 import android.graphics.Rect; 27 import android.graphics.drawable.Drawable; 28 import android.support.v4.view.accessibility.AccessibilityRecordCompat; 29 import android.support.v4.view.accessibility.AccessibilityEventCompat; 30 import android.net.Uri; 31 import android.support.v7.widget.GridLayoutManager; 32 import android.support.v7.widget.RecyclerView; 33 import android.view.Gravity; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewConfiguration; 37 import android.view.ViewGroup; 38 import android.view.accessibility.AccessibilityEvent; 39 import android.widget.TextView; 40 import com.android.launcher3.AppInfo; 41 import com.android.launcher3.BubbleTextView; 42 import com.android.launcher3.Launcher; 43 import com.android.launcher3.R; 44 import com.android.launcher3.Utilities; 45 import com.android.launcher3.util.Thunk; 46 47 import java.util.HashMap; 48 import java.util.List; 49 50 51 /** 52 * The grid view adapter of all the apps. 53 */ 54 public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { 55 56 public static final String TAG = "AppsGridAdapter"; 57 private static final boolean DEBUG = false; 58 59 // A section break in the grid 60 public static final int SECTION_BREAK_VIEW_TYPE = 0; 61 // A normal icon 62 public static final int ICON_VIEW_TYPE = 1; 63 // A prediction icon 64 public static final int PREDICTION_ICON_VIEW_TYPE = 2; 65 // The message shown when there are no filtered results 66 public static final int EMPTY_SEARCH_VIEW_TYPE = 3; 67 // A divider that separates the apps list and the search market button 68 public static final int SEARCH_MARKET_DIVIDER_VIEW_TYPE = 4; 69 // The message to continue to a market search when there are no filtered results 70 public static final int SEARCH_MARKET_VIEW_TYPE = 5; 71 72 /** 73 * ViewHolder for each icon. 74 */ 75 public static class ViewHolder extends RecyclerView.ViewHolder { 76 public View mContent; 77 ViewHolder(View v)78 public ViewHolder(View v) { 79 super(v); 80 mContent = v; 81 } 82 } 83 84 /** 85 * A subclass of GridLayoutManager that overrides accessibility values during app search. 86 */ 87 public class AppsGridLayoutManager extends GridLayoutManager { 88 AppsGridLayoutManager(Context context)89 public AppsGridLayoutManager(Context context) { 90 super(context, 1, GridLayoutManager.VERTICAL, false); 91 } 92 93 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)94 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 95 super.onInitializeAccessibilityEvent(event); 96 97 // Ensure that we only report the number apps for accessibility not including other 98 // adapter views 99 final AccessibilityRecordCompat record = AccessibilityEventCompat 100 .asRecord(event); 101 record.setItemCount(mApps.getNumFilteredApps()); 102 } 103 104 @Override getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)105 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 106 RecyclerView.State state) { 107 if (mApps.hasNoFilteredResults()) { 108 // Disregard the no-search-results text as a list item for accessibility 109 return 0; 110 } else { 111 return super.getRowCountForAccessibility(recycler, state); 112 } 113 } 114 } 115 116 /** 117 * Helper class to size the grid items. 118 */ 119 public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { 120 GridSpanSizer()121 public GridSpanSizer() { 122 super(); 123 setSpanIndexCacheEnabled(true); 124 } 125 126 @Override getSpanSize(int position)127 public int getSpanSize(int position) { 128 switch (mApps.getAdapterItems().get(position).viewType) { 129 case AllAppsGridAdapter.ICON_VIEW_TYPE: 130 case AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE: 131 return 1; 132 default: 133 // Section breaks span the full width 134 return mAppsPerRow; 135 } 136 } 137 } 138 139 /** 140 * Helper class to draw the section headers 141 */ 142 public class GridItemDecoration extends RecyclerView.ItemDecoration { 143 144 private static final boolean DEBUG_SECTION_MARGIN = false; 145 private static final boolean FADE_OUT_SECTIONS = false; 146 147 private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>(); 148 private Rect mTmpBounds = new Rect(); 149 150 @Override onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)151 public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { 152 if (mApps.hasFilter() || mAppsPerRow == 0) { 153 return; 154 } 155 156 if (DEBUG_SECTION_MARGIN) { 157 Paint p = new Paint(); 158 p.setColor(0x33ff0000); 159 c.drawRect(mBackgroundPadding.left, 0, mBackgroundPadding.left + mSectionNamesMargin, 160 parent.getMeasuredHeight(), p); 161 } 162 163 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 164 boolean hasDrawnPredictedAppsDivider = false; 165 boolean showSectionNames = mSectionNamesMargin > 0; 166 int childCount = parent.getChildCount(); 167 int lastSectionTop = 0; 168 int lastSectionHeight = 0; 169 for (int i = 0; i < childCount; i++) { 170 View child = parent.getChildAt(i); 171 ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child); 172 if (!isValidHolderAndChild(holder, child, items)) { 173 continue; 174 } 175 176 if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppsDivider) { 177 // Draw the divider under the predicted apps 178 int top = child.getTop() + child.getHeight() + mPredictionBarDividerOffset; 179 c.drawLine(mBackgroundPadding.left, top, 180 parent.getWidth() - mBackgroundPadding.right, top, 181 mPredictedAppsDividerPaint); 182 hasDrawnPredictedAppsDivider = true; 183 184 } else if (showSectionNames && shouldDrawItemSection(holder, i, items)) { 185 // At this point, we only draw sections for each section break; 186 int viewTopOffset = (2 * child.getPaddingTop()); 187 int pos = holder.getPosition(); 188 AlphabeticalAppsList.AdapterItem item = items.get(pos); 189 AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo; 190 191 // Draw all the sections for this index 192 String lastSectionName = item.sectionName; 193 for (int j = item.sectionAppIndex; j < sectionInfo.numApps; j++, pos++) { 194 AlphabeticalAppsList.AdapterItem nextItem = items.get(pos); 195 String sectionName = nextItem.sectionName; 196 if (nextItem.sectionInfo != sectionInfo) { 197 break; 198 } 199 if (j > item.sectionAppIndex && sectionName.equals(lastSectionName)) { 200 continue; 201 } 202 203 204 // Find the section name bounds 205 PointF sectionBounds = getAndCacheSectionBounds(sectionName); 206 207 // Calculate where to draw the section 208 int sectionBaseline = (int) (viewTopOffset + sectionBounds.y); 209 int x = mIsRtl ? 210 parent.getWidth() - mBackgroundPadding.left - mSectionNamesMargin : 211 mBackgroundPadding.left; 212 x += (int) ((mSectionNamesMargin - sectionBounds.x) / 2f); 213 int y = child.getTop() + sectionBaseline; 214 215 // Determine whether this is the last row with apps in that section, if 216 // so, then fix the section to the row allowing it to scroll past the 217 // baseline, otherwise, bound it to the baseline so it's in the viewport 218 int appIndexInSection = items.get(pos).sectionAppIndex; 219 int nextRowPos = Math.min(items.size() - 1, 220 pos + mAppsPerRow - (appIndexInSection % mAppsPerRow)); 221 AlphabeticalAppsList.AdapterItem nextRowItem = items.get(nextRowPos); 222 boolean fixedToRow = !sectionName.equals(nextRowItem.sectionName); 223 if (!fixedToRow) { 224 y = Math.max(sectionBaseline, y); 225 } 226 227 // In addition, if it overlaps with the last section that was drawn, then 228 // offset it so that it does not overlap 229 if (lastSectionHeight > 0 && y <= (lastSectionTop + lastSectionHeight)) { 230 y += lastSectionTop - y + lastSectionHeight; 231 } 232 233 // Draw the section header 234 if (FADE_OUT_SECTIONS) { 235 int alpha = 255; 236 if (fixedToRow) { 237 alpha = Math.min(255, 238 (int) (255 * (Math.max(0, y) / (float) sectionBaseline))); 239 } 240 mSectionTextPaint.setAlpha(alpha); 241 } 242 c.drawText(sectionName, x, y, mSectionTextPaint); 243 244 lastSectionTop = y; 245 lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset); 246 lastSectionName = sectionName; 247 } 248 i += (sectionInfo.numApps - item.sectionAppIndex); 249 } 250 } 251 } 252 253 @Override getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)254 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 255 RecyclerView.State state) { 256 // Do nothing 257 } 258 259 /** 260 * Given a section name, return the bounds of the given section name. 261 */ getAndCacheSectionBounds(String sectionName)262 private PointF getAndCacheSectionBounds(String sectionName) { 263 PointF bounds = mCachedSectionBounds.get(sectionName); 264 if (bounds == null) { 265 mSectionTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTmpBounds); 266 bounds = new PointF(mSectionTextPaint.measureText(sectionName), mTmpBounds.height()); 267 mCachedSectionBounds.put(sectionName, bounds); 268 } 269 return bounds; 270 } 271 272 /** 273 * Returns whether we consider this a valid view holder for us to draw a divider or section for. 274 */ isValidHolderAndChild(ViewHolder holder, View child, List<AlphabeticalAppsList.AdapterItem> items)275 private boolean isValidHolderAndChild(ViewHolder holder, View child, 276 List<AlphabeticalAppsList.AdapterItem> items) { 277 // Ensure item is not already removed 278 GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) 279 child.getLayoutParams(); 280 if (lp.isItemRemoved()) { 281 return false; 282 } 283 // Ensure we have a valid holder 284 if (holder == null) { 285 return false; 286 } 287 // Ensure we have a holder position 288 int pos = holder.getPosition(); 289 if (pos < 0 || pos >= items.size()) { 290 return false; 291 } 292 return true; 293 } 294 295 /** 296 * Returns whether to draw the divider for a given child. 297 */ shouldDrawItemDivider(ViewHolder holder, List<AlphabeticalAppsList.AdapterItem> items)298 private boolean shouldDrawItemDivider(ViewHolder holder, 299 List<AlphabeticalAppsList.AdapterItem> items) { 300 int pos = holder.getPosition(); 301 return items.get(pos).viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE; 302 } 303 304 /** 305 * Returns whether to draw the section for the given child. 306 */ shouldDrawItemSection(ViewHolder holder, int childIndex, List<AlphabeticalAppsList.AdapterItem> items)307 private boolean shouldDrawItemSection(ViewHolder holder, int childIndex, 308 List<AlphabeticalAppsList.AdapterItem> items) { 309 int pos = holder.getPosition(); 310 AlphabeticalAppsList.AdapterItem item = items.get(pos); 311 312 // Ensure it's an icon 313 if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) { 314 return false; 315 } 316 // Draw the section header for the first item in each section 317 return (childIndex == 0) || 318 (items.get(pos - 1).viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE); 319 } 320 } 321 322 private Launcher mLauncher; 323 private LayoutInflater mLayoutInflater; 324 @Thunk AlphabeticalAppsList mApps; 325 private GridLayoutManager mGridLayoutMgr; 326 private GridSpanSizer mGridSizer; 327 private GridItemDecoration mItemDecoration; 328 private View.OnTouchListener mTouchListener; 329 private View.OnClickListener mIconClickListener; 330 private View.OnLongClickListener mIconLongClickListener; 331 @Thunk final Rect mBackgroundPadding = new Rect(); 332 @Thunk int mPredictionBarDividerOffset; 333 @Thunk int mAppsPerRow; 334 @Thunk boolean mIsRtl; 335 336 // The text to show when there are no search results and no market search handler. 337 private String mEmptySearchMessage; 338 // The name of the market app which handles searches, to be used in the format str 339 // below when updating the search-market view. Only needs to be loaded once. 340 private String mMarketAppName; 341 // The text to show when there is a market app which can handle a specific query, updated 342 // each time the search query changes. 343 private String mMarketSearchMessage; 344 // The intent to send off to the market app, updated each time the search query changes. 345 private Intent mMarketSearchIntent; 346 // The last query that the user entered into the search field 347 private String mLastSearchQuery; 348 349 // Section drawing 350 @Thunk int mSectionNamesMargin; 351 @Thunk int mSectionHeaderOffset; 352 @Thunk Paint mSectionTextPaint; 353 @Thunk Paint mPredictedAppsDividerPaint; 354 AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnTouchListener touchListener, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener)355 public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, 356 View.OnTouchListener touchListener, View.OnClickListener iconClickListener, 357 View.OnLongClickListener iconLongClickListener) { 358 Resources res = launcher.getResources(); 359 mLauncher = launcher; 360 mApps = apps; 361 mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); 362 mGridSizer = new GridSpanSizer(); 363 mGridLayoutMgr = new AppsGridLayoutManager(launcher); 364 mGridLayoutMgr.setSpanSizeLookup(mGridSizer); 365 mItemDecoration = new GridItemDecoration(); 366 mLayoutInflater = LayoutInflater.from(launcher); 367 mTouchListener = touchListener; 368 mIconClickListener = iconClickListener; 369 mIconLongClickListener = iconLongClickListener; 370 mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); 371 mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset); 372 373 mSectionTextPaint = new Paint(); 374 mSectionTextPaint.setTextSize(res.getDimensionPixelSize( 375 R.dimen.all_apps_grid_section_text_size)); 376 mSectionTextPaint.setColor(res.getColor(R.color.all_apps_grid_section_text_color)); 377 mSectionTextPaint.setAntiAlias(true); 378 379 mPredictedAppsDividerPaint = new Paint(); 380 mPredictedAppsDividerPaint.setStrokeWidth(Utilities.pxFromDp(1f, res.getDisplayMetrics())); 381 mPredictedAppsDividerPaint.setColor(0x1E000000); 382 mPredictedAppsDividerPaint.setAntiAlias(true); 383 mPredictionBarDividerOffset = 384 (-res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_bottom_padding) + 385 res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding)) / 2; 386 387 // Resolve the market app handling additional searches 388 PackageManager pm = launcher.getPackageManager(); 389 ResolveInfo marketInfo = pm.resolveActivity(createMarketSearchIntent(""), 390 PackageManager.MATCH_DEFAULT_ONLY); 391 if (marketInfo != null) { 392 mMarketAppName = marketInfo.loadLabel(pm).toString(); 393 } 394 } 395 396 /** 397 * Sets the number of apps per row. 398 */ setNumAppsPerRow(int appsPerRow)399 public void setNumAppsPerRow(int appsPerRow) { 400 mAppsPerRow = appsPerRow; 401 mGridLayoutMgr.setSpanCount(appsPerRow); 402 } 403 404 /** 405 * Sets whether we are in RTL mode. 406 */ setRtl(boolean rtl)407 public void setRtl(boolean rtl) { 408 mIsRtl = rtl; 409 } 410 411 /** 412 * Sets the last search query that was made, used to show when there are no results and to also 413 * seed the intent for searching the market. 414 */ setLastSearchQuery(String query)415 public void setLastSearchQuery(String query) { 416 Resources res = mLauncher.getResources(); 417 String formatStr = res.getString(R.string.all_apps_no_search_results); 418 mLastSearchQuery = query; 419 mEmptySearchMessage = String.format(formatStr, query); 420 if (mMarketAppName != null) { 421 mMarketSearchMessage = String.format(res.getString(R.string.all_apps_search_market_message), 422 mMarketAppName); 423 mMarketSearchIntent = createMarketSearchIntent(query); 424 } 425 } 426 427 /** 428 * Notifies the adapter of the background padding so that it can draw things correctly in the 429 * item decorator. 430 */ updateBackgroundPadding(Rect padding)431 public void updateBackgroundPadding(Rect padding) { 432 mBackgroundPadding.set(padding); 433 } 434 435 /** 436 * Returns the grid layout manager. 437 */ getLayoutManager()438 public GridLayoutManager getLayoutManager() { 439 return mGridLayoutMgr; 440 } 441 442 /** 443 * Returns the item decoration for the recycler view. 444 */ getItemDecoration()445 public RecyclerView.ItemDecoration getItemDecoration() { 446 // We don't draw any headers when we are uncomfortably dense 447 return mItemDecoration; 448 } 449 450 @Override onCreateViewHolder(ViewGroup parent, int viewType)451 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 452 switch (viewType) { 453 case SECTION_BREAK_VIEW_TYPE: 454 return new ViewHolder(new View(parent.getContext())); 455 case ICON_VIEW_TYPE: { 456 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( 457 R.layout.all_apps_icon, parent, false); 458 icon.setOnTouchListener(mTouchListener); 459 icon.setOnClickListener(mIconClickListener); 460 icon.setOnLongClickListener(mIconLongClickListener); 461 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) 462 .getLongPressTimeout()); 463 icon.setFocusable(true); 464 return new ViewHolder(icon); 465 } 466 case PREDICTION_ICON_VIEW_TYPE: { 467 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( 468 R.layout.all_apps_prediction_bar_icon, parent, false); 469 icon.setOnTouchListener(mTouchListener); 470 icon.setOnClickListener(mIconClickListener); 471 icon.setOnLongClickListener(mIconLongClickListener); 472 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) 473 .getLongPressTimeout()); 474 icon.setFocusable(true); 475 return new ViewHolder(icon); 476 } 477 case EMPTY_SEARCH_VIEW_TYPE: 478 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, 479 parent, false)); 480 case SEARCH_MARKET_DIVIDER_VIEW_TYPE: 481 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_search_market_divider, 482 parent, false)); 483 case SEARCH_MARKET_VIEW_TYPE: 484 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, 485 parent, false); 486 searchMarketView.setOnClickListener(new View.OnClickListener() { 487 @Override 488 public void onClick(View v) { 489 mLauncher.startSearchFromAllApps(v, mMarketSearchIntent, mLastSearchQuery); 490 } 491 }); 492 return new ViewHolder(searchMarketView); 493 default: 494 throw new RuntimeException("Unexpected view type"); 495 } 496 } 497 498 @Override onBindViewHolder(ViewHolder holder, int position)499 public void onBindViewHolder(ViewHolder holder, int position) { 500 switch (holder.getItemViewType()) { 501 case ICON_VIEW_TYPE: { 502 AppInfo info = mApps.getAdapterItems().get(position).appInfo; 503 BubbleTextView icon = (BubbleTextView) holder.mContent; 504 icon.applyFromApplicationInfo(info); 505 break; 506 } 507 case PREDICTION_ICON_VIEW_TYPE: { 508 AppInfo info = mApps.getAdapterItems().get(position).appInfo; 509 BubbleTextView icon = (BubbleTextView) holder.mContent; 510 icon.applyFromApplicationInfo(info); 511 break; 512 } 513 case EMPTY_SEARCH_VIEW_TYPE: 514 TextView emptyViewText = (TextView) holder.mContent; 515 emptyViewText.setText(mEmptySearchMessage); 516 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : 517 Gravity.START | Gravity.CENTER_VERTICAL); 518 break; 519 case SEARCH_MARKET_VIEW_TYPE: 520 TextView searchView = (TextView) holder.mContent; 521 if (mMarketSearchIntent != null) { 522 searchView.setVisibility(View.VISIBLE); 523 searchView.setContentDescription(mMarketSearchMessage); 524 searchView.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : 525 Gravity.START | Gravity.CENTER_VERTICAL); 526 searchView.setText(mMarketSearchMessage); 527 } else { 528 searchView.setVisibility(View.GONE); 529 } 530 break; 531 } 532 } 533 534 @Override getItemCount()535 public int getItemCount() { 536 return mApps.getAdapterItems().size(); 537 } 538 539 @Override getItemViewType(int position)540 public int getItemViewType(int position) { 541 AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); 542 return item.viewType; 543 } 544 545 /** 546 * Creates a new market search intent. 547 */ createMarketSearchIntent(String query)548 private Intent createMarketSearchIntent(String query) { 549 Uri marketSearchUri = Uri.parse("market://search") 550 .buildUpon() 551 .appendQueryParameter("q", query) 552 .build(); 553 Intent marketSearchIntent = new Intent(Intent.ACTION_VIEW); 554 marketSearchIntent.setData(marketSearchUri); 555 return marketSearchIntent; 556 } 557 } 558