1 package com.example.android.leanback;
2 
3 import static com.example.android.leanback.CardPresenter.CONTENT;
4 import static com.example.android.leanback.CardPresenter.IMAGE;
5 import static com.example.android.leanback.CardPresenter.TITLE;
6 
7 import android.content.Context;
8 import android.content.Intent;
9 import android.os.Bundle;
10 import android.os.Handler;
11 import android.text.TextUtils;
12 import android.util.Log;
13 
14 import androidx.core.app.ActivityOptionsCompat;
15 import androidx.core.content.res.ResourcesCompat;
16 import androidx.leanback.widget.ArrayObjectAdapter;
17 import androidx.leanback.widget.DiffCallback;
18 import androidx.leanback.widget.HeaderItem;
19 import androidx.leanback.widget.ImageCardView;
20 import androidx.leanback.widget.ListRow;
21 import androidx.leanback.widget.ListRowPresenter;
22 import androidx.leanback.widget.ObjectAdapter;
23 import androidx.leanback.widget.OnItemViewClickedListener;
24 import androidx.leanback.widget.Presenter;
25 import androidx.leanback.widget.Row;
26 import androidx.leanback.widget.RowPresenter;
27 
28 import org.jspecify.annotations.NonNull;
29 import org.jspecify.annotations.Nullable;
30 
31 import java.util.ArrayList;
32 
33 public class SearchFragment extends androidx.leanback.app.SearchFragment
34         implements androidx.leanback.app.SearchFragment.SearchResultProvider {
35     private static final String TAG = "leanback.SearchFragment";
36     private static final int NUM_ROWS = 3;
37     private static final int SEARCH_DELAY_MS = 1000;
38 
39     private ArrayObjectAdapter mRowsAdapter;
40     private Handler mHandler = new Handler();
41     private String mQuery;
42 
43     // Flag to represent if data set one is presented in the fragment
44     private boolean mIsDataSetOnePresented;
45 
46     // Adapter for first row
47     private ArrayObjectAdapter mFirstRowAdapter;
48 
49     // The diff callback which defines the standard to judge if two items are the same or if
50     // two items have the same content.
51     private DiffCallback<PhotoItem> mDiffCallback = new DiffCallback<PhotoItem>() {
52 
53         // when two photo items have the same id, they are the same from adapter's
54         // perspective
55         @Override
56         public boolean areItemsTheSame(@NonNull PhotoItem oldItem, @NonNull PhotoItem newItem) {
57             return oldItem.getId() == newItem.getId();
58         }
59 
60         // when two photo items is equal to each other (based on the equal method defined in
61         // PhotoItem), they have the same content.
62         @Override
63         public boolean areContentsTheSame(@NonNull PhotoItem oldItem, @NonNull PhotoItem newItem) {
64             return oldItem.equals(newItem);
65         }
66 
67         @Override
68         public @Nullable Object getChangePayload(@NonNull PhotoItem oldItem,
69                 @NonNull PhotoItem newItem) {
70             Bundle diff = new Bundle();
71             if (oldItem.getImageResourceId()
72                     != newItem.getImageResourceId()) {
73                 diff.putLong(IMAGE, newItem.getImageResourceId());
74             }
75 
76             if (oldItem.getTitle() != null && newItem.getTitle() != null
77                     && !oldItem.getTitle().equals(newItem.getTitle())) {
78                 diff.putString(TITLE, newItem.getTitle());
79             }
80 
81             if (oldItem.getContent() != null && newItem.getContent() != null
82                     && !oldItem.getContent().equals(newItem.getContent())) {
83                 diff.putString(CONTENT, newItem.getContent());
84             }
85             return diff;
86         }
87     };
88 
89     @Override
onCreate(Bundle savedInstanceState)90     public void onCreate(Bundle savedInstanceState) {
91         super.onCreate(savedInstanceState);
92 
93         mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
94 
95         final Context context = getActivity();
96         setBadgeDrawable(ResourcesCompat.getDrawable(context.getResources(),
97                 R.drawable.ic_title, context.getTheme()));
98         setTitle("Leanback Sample App");
99         setSearchResultProvider(this);
100         setOnItemViewClickedListener(new ItemViewClickedListener());
101     }
102 
103     @Override
getResultsAdapter()104     public ObjectAdapter getResultsAdapter() {
105         return mRowsAdapter;
106     }
107 
108     @Override
onQueryTextChange(String newQuery)109     public boolean onQueryTextChange(String newQuery) {
110         Log.i(TAG, String.format("Search Query Text Change %s", newQuery));
111         mRowsAdapter.clear();
112         loadQuery(newQuery);
113         return true;
114     }
115 
116     @Override
onQueryTextSubmit(String query)117     public boolean onQueryTextSubmit(String query) {
118         Log.i(TAG, String.format("Search Query Text Submit %s", query));
119         mRowsAdapter.clear();
120         loadQuery(query);
121         return true;
122     }
123 
loadQuery(String query)124     private void loadQuery(String query) {
125         mQuery = query;
126         mHandler.removeCallbacks(mDelayedLoad);
127         if (!TextUtils.isEmpty(query) && !query.equals("nil")) {
128             mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
129         }
130     }
131 
loadRows()132     private void loadRows() {
133         HeaderItem header = new HeaderItem(0, mQuery + " results row " + 0);
134 
135         // Every time when the query event is fired, we will update the fake search result in the
136         // first row based on the flag mIsDataSetOnePresented flag.
137         // Also the first row adapter will only be created once so the animation will be triggered
138         // when the items in the adapter changed.
139         if (!mIsDataSetOnePresented) {
140             if (mFirstRowAdapter == null) {
141                 mFirstRowAdapter = createFirstListRowAdapter();
142             } else {
143                 mFirstRowAdapter.setItems(createDataSetOneDebug(), mDiffCallback);
144             }
145             mIsDataSetOnePresented = true;
146         } else {
147             mFirstRowAdapter.setItems(createDataSetTwoDebug(), mDiffCallback);
148             mIsDataSetOnePresented = false;
149         }
150         mRowsAdapter.add(new ListRow(header, mFirstRowAdapter));
151         for (int i = 1; i < NUM_ROWS + 1; ++i) {
152             ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter());
153             listRowAdapter.add(new PhotoItem("Hello world", R.drawable.gallery_photo_1));
154             listRowAdapter.add(new PhotoItem("This is a test", R.drawable.gallery_photo_2));
155             header = new HeaderItem(i, mQuery + " results row " + i);
156             mRowsAdapter.add(new ListRow(header, listRowAdapter));
157         }
158     }
159 
160     private Runnable mDelayedLoad = new Runnable() {
161         @Override
162         public void run() {
163             loadRows();
164         }
165     };
166 
167     private final class ItemViewClickedListener implements OnItemViewClickedListener {
168         @Override
onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row)169         public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
170                 RowPresenter.ViewHolder rowViewHolder, Row row) {
171             Intent intent = new Intent(getActivity(), DetailsActivity.class);
172             intent.putExtra(DetailsActivity.EXTRA_ITEM, (PhotoItem) item);
173 
174             Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
175                     getActivity(),
176                     ((ImageCardView) itemViewHolder.view).getMainImageView(),
177                     DetailsActivity.SHARED_ELEMENT_NAME).toBundle();
178             getActivity().startActivity(intent, bundle);
179         }
180     }
181 
182 
createFirstListRowAdapter()183     private ArrayObjectAdapter createFirstListRowAdapter() {
184         ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter());
185         listRowAdapter.setItems(createDataSetOneDebug(), mDiffCallback);
186         mIsDataSetOnePresented = true;
187         return listRowAdapter;
188     }
189 
createDataSetOneDebug()190     private ArrayList<PhotoItem> createDataSetOneDebug() {
191         ArrayList<PhotoItem> photoItems = new ArrayList<>();
192         photoItems.add(new PhotoItem(
193                 "Hello world",
194                 R.drawable.gallery_photo_1,
195                 1));
196         return photoItems;
197     }
198 
199     /**
200      * Create a new data set (data set one) for the last row of this browse fragment. It will be
201      * changed by another set of data when user click one of the photo items in the list.
202      * Different with other rows in the browsing fragment, the photo item in last row all have been
203      * allocated with a unique id. And the id will be used to jduge if two photo items are the same
204      * or not.
205      *
206      * @return List of photoItem
207      */
createDataSetTwoDebug()208     private ArrayList<PhotoItem> createDataSetTwoDebug() {
209         ArrayList<PhotoItem> photoItems = new ArrayList<>();
210         photoItems.add(new PhotoItem(
211                 "Hello world Hello world",
212                 R.drawable.gallery_photo_1,
213                 1));
214         return photoItems;
215     }
216 }
217