1 /*
2  * Copyright (C) 2016 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 
17 package com.example.sampleleanbacklauncher.apps;
18 
19 import android.app.Fragment;
20 import android.app.LoaderManager;
21 import android.content.ActivityNotFoundException;
22 import android.content.BroadcastReceiver;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.CursorLoader;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.Loader;
29 import android.content.ServiceConnection;
30 import android.database.Cursor;
31 import android.database.DatabaseUtils;
32 import android.os.Bundle;
33 import android.os.IBinder;
34 import androidx.annotation.IdRes;
35 import androidx.annotation.MainThread;
36 import androidx.annotation.Nullable;
37 import androidx.annotation.StringDef;
38 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
39 import androidx.collection.ArrayMap;
40 import androidx.recyclerview.widget.SortedList;
41 import androidx.recyclerview.widget.RecyclerView;
42 import androidx.recyclerview.widget.SortedListAdapterCallback;
43 import android.util.Log;
44 import android.view.LayoutInflater;
45 import android.view.View;
46 import android.view.ViewGroup;
47 import android.widget.ImageView;
48 import android.widget.Toast;
49 import android.widget.TextView;
50 
51 import com.example.sampleleanbacklauncher.R;
52 import com.example.sampleleanbacklauncher.notifications.NotificationsContract;
53 import com.example.sampleleanbacklauncher.util.LauncherAsyncTaskLoader;
54 
55 import java.lang.annotation.Retention;
56 import java.lang.annotation.RetentionPolicy;
57 import java.util.Map;
58 import java.util.Objects;
59 import java.util.Set;
60 
61 
62 public class AppFragment extends Fragment {
63     @Retention(RetentionPolicy.SOURCE)
64     @StringDef({ROW_TYPE_APPS, ROW_TYPE_GAMES, ROW_TYPE_SETTINGS})
65     public @interface RowType {}
66     public static final String ROW_TYPE_APPS = "apps";
67     public static final String ROW_TYPE_GAMES = "games";
68     public static final String ROW_TYPE_SETTINGS = "settings";
69 
70     private static final String TAG = "AppFragment";
71     private static final boolean DEBUG = false;
72 
73     private static final String ARG_ROW_TYPE = "AppFragment.ROW_TYPE";
74 
75     private static final int ITEM_LOADER_ID = 1;
76     private static final int NOTIFS_COUNT_LOADER_ID = 2;
77 
78     @RowType
79     private String mRowType;
80 
81     private AppAdapter mAdapter;
82 
83     private LaunchItemsManager mLaunchItemsManager;
84     private final ServiceConnection mLaunchItemsServiceConnection = new ServiceConnection() {
85         @Override
86         public void onServiceConnected(ComponentName componentName, IBinder service) {
87             mLaunchItemsManager =
88                     ((LaunchItemsManager.LocalBinder) service).getLaunchItemsManager();
89         }
90 
91         @Override
92         public void onServiceDisconnected(ComponentName componentName) {
93             mLaunchItemsManager = null;
94         }
95     };
96 
newInstance(@owType String rowType)97     public static AppFragment newInstance(@RowType String rowType) {
98         Bundle args = new Bundle(1);
99         args.putString(ARG_ROW_TYPE, rowType);
100 
101         AppFragment fragment = new AppFragment();
102         fragment.setArguments(args);
103         return fragment;
104     }
105 
106     @Override
onCreate(Bundle savedInstanceState)107     public void onCreate(Bundle savedInstanceState) {
108         super.onCreate(savedInstanceState);
109         //noinspection WrongConstant
110         mRowType = getArguments().getString(ARG_ROW_TYPE, ROW_TYPE_APPS);
111         final Context context = getContext();
112         context.bindService(new Intent(context, LaunchItemsManager.class),
113                 mLaunchItemsServiceConnection, Context.BIND_AUTO_CREATE);
114     }
115 
116     @Nullable
117     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)118     public View onCreateView(LayoutInflater inflater, ViewGroup container,
119                              Bundle savedInstanceState) {
120         final View root = inflater.inflate(R.layout.app_launch_row, container, false);
121         // Since there's multiple instances of this root view in the parent, they need a unique
122         // id so that view state save/restore works correctly.
123         root.setId(getRootViewId());
124         final TextView rowTitle = (TextView) root.findViewById(R.id.row_title);
125         rowTitle.setText(getRowTitle());
126         final RecyclerView list = (RecyclerView) root.findViewById(android.R.id.list);
127         mAdapter = new AppAdapter();
128         getLoaderManager().initLoader(ITEM_LOADER_ID, null, new ItemLoaderCallbacks());
129         list.setAdapter(mAdapter);
130 
131         if (mRowType == ROW_TYPE_SETTINGS) {
132             getLoaderManager().initLoader(NOTIFS_COUNT_LOADER_ID, null,
133                     new NotifsCountLoaderCallbacks());
134         }
135 
136         return root;
137     }
138 
139     @Override
onDestroy()140     public void onDestroy() {
141         super.onDestroy();
142         Context context = getContext();
143         context.unbindService(mLaunchItemsServiceConnection);
144     }
145 
getRootViewId()146     private @IdRes int getRootViewId() {
147         switch (mRowType) {
148             case ROW_TYPE_APPS:
149                 return R.id.apps_row;
150             case ROW_TYPE_GAMES:
151                 return R.id.games_row;
152             case ROW_TYPE_SETTINGS:
153                 return R.id.settings_row;
154         }
155         throw new IllegalStateException("Unknown row type");
156     }
157 
getRowTitle()158     public CharSequence getRowTitle() {
159         switch (mRowType) {
160             case ROW_TYPE_APPS:
161                 return getText(R.string.apps_row_title);
162             case ROW_TYPE_GAMES:
163                 return getText(R.string.games_row_title);
164             case ROW_TYPE_SETTINGS:
165                 return getText(R.string.settings_row_title);
166         }
167         throw new IllegalStateException("Unknown row type");
168     }
169 
170     @MainThread
updateList(Set<LaunchItem> newItems)171     private void updateList(Set<LaunchItem> newItems) {
172         if (!isAdded() || mAdapter == null) {
173             return;
174         }
175 
176         final SortedList<LaunchItem> items = mAdapter.getLaunchItems();
177 
178         if (newItems == null) {
179             items.clear();
180             return;
181         }
182 
183         items.beginBatchedUpdates();
184         try {
185             for (int i = 0; i < items.size(); ) {
186                 final LaunchItem item = items.get(i);
187                 if (newItems.contains(item)) {
188                     i++;
189                 } else {
190                     items.remove(item);
191                 }
192             }
193             items.addAll(newItems);
194         } finally {
195             items.endBatchedUpdates();
196         }
197     }
198 
onItemClicked(int adapterPosition)199     void onItemClicked(int adapterPosition) {
200         SortedList<LaunchItem> launchItems = mAdapter.getLaunchItems();
201         if (adapterPosition >= launchItems.size()) {
202             Log.e(TAG, "Item clicked out of bounds, index " + adapterPosition +
203                     " size " + launchItems.size());
204             return;
205         }
206         final LaunchItem item = launchItems.get(adapterPosition);
207         try {
208             startActivity(item.getIntent());
209             if (mLaunchItemsManager != null) {
210                 mLaunchItemsManager.notifyItemLaunched(item);
211             }
212         } catch (ActivityNotFoundException e) {
213             Log.e(TAG, "Exception launching intent " + item.getIntent(), e);
214             Toast.makeText(getContext(), getString(R.string.app_unavailable),
215                     Toast.LENGTH_SHORT).show();
216         }
217     }
218 
219     public class AppViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
220         private final View mView;
221 
AppViewHolder(View v)222         public AppViewHolder(View v) {
223             super(v);
224             mView = v;
225         }
226 
bind(LaunchItem launchItem)227         public void bind(LaunchItem launchItem) {
228             mView.findViewById(R.id.frame).setContentDescription(launchItem.getLabel());
229             mView.findViewById(R.id.frame).setOnClickListener(this);
230 
231             mView.findViewById(R.id.banner).setVisibility(View.VISIBLE);
232 
233             mView.findViewById(R.id.ll).setVisibility(View.VISIBLE);
234 
235             TextView launchItemLabel = mView.findViewById(R.id.label);
236             launchItemLabel.setText(launchItem.getLabel());
237 
238             ImageView launchItemIcon = mView.findViewById(R.id.icon);
239             launchItemIcon.setImageDrawable(launchItem.getIcon());
240         }
241 
242         @Override
onClick(View v)243         public void onClick(View v) {
244             final int position = getAdapterPosition();
245             if (position != RecyclerView.NO_POSITION) {
246                 onItemClicked(position);
247             }
248         }
249     }
250 
251     public class AppAdapter extends RecyclerView.Adapter<AppViewHolder> {
252         private final SortedList<LaunchItem> mLaunchItems;
253         private final Map<ComponentName, Long> mItemIdMap = new ArrayMap<>();
254         private long mNextItemId = 0;
255 
AppAdapter()256         public AppAdapter() {
257             mLaunchItems = new SortedList<>(LaunchItem.class,
258                     new SortedListAdapterCallback<LaunchItem>(this) {
259                         @Override
260                         public int compare(LaunchItem o1, LaunchItem o2) {
261                             return o1.compareTo(o2);
262                         }
263 
264                         @Override
265                         public boolean areContentsTheSame(LaunchItem oldItem, LaunchItem newItem) {
266                             return oldItem.areContentsTheSame(newItem);
267                         }
268 
269                         @Override
270                         public boolean areItemsTheSame(LaunchItem item1, LaunchItem item2) {
271                             return Objects.equals(item1, item2);
272                         }
273                     });
274             setHasStableIds(true);
275         }
276 
277         @Override
onCreateViewHolder(ViewGroup parent, int viewType)278         public AppViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
279             final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
280             return new AppViewHolder(inflater.inflate(R.layout.launch_item, parent, false));
281         }
282 
283         @Override
onBindViewHolder(AppViewHolder holder, int position)284         public void onBindViewHolder(AppViewHolder holder, int position) {
285             holder.bind(mLaunchItems.get(position));
286         }
287 
288         @Override
getItemCount()289         public int getItemCount() {
290             return mLaunchItems.size();
291         }
292 
getLaunchItems()293         public SortedList<LaunchItem> getLaunchItems() {
294             return mLaunchItems;
295         }
296 
297         @Override
getItemId(int position)298         public long getItemId(int position) {
299             ComponentName componentName = mLaunchItems.get(position).getIntent().getComponent();
300             Long id = mItemIdMap.get(componentName);
301             if (id != null) {
302                 return id;
303             } else {
304                 long newId = mNextItemId++;
305                 mItemIdMap.put(componentName, newId);
306                 return newId;
307             }
308         }
309     }
310 
311     private class ItemLoaderCallbacks implements LoaderManager.LoaderCallbacks<Set<LaunchItem>> {
312         @Override
onCreateLoader(int id, Bundle args)313         public Loader<Set<LaunchItem>> onCreateLoader(int id, Bundle args) {
314             return new ItemLoader(getContext(), mRowType);
315         }
316 
317         @Override
onLoadFinished(Loader<Set<LaunchItem>> loader, Set<LaunchItem> data)318         public void onLoadFinished(Loader<Set<LaunchItem>> loader, Set<LaunchItem> data) {
319             updateList(data);
320         }
321 
322         @Override
onLoaderReset(Loader<Set<LaunchItem>> loader)323         public void onLoaderReset(Loader<Set<LaunchItem>> loader) {}
324     }
325 
326     private static class ItemLoader extends LauncherAsyncTaskLoader<Set<LaunchItem>> {
327 
328         private final String mRowType;
329 
330         private LaunchItemsManager mLaunchItemsManager;
331         private ServiceConnection mConnection;
332 
333         private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
334             @Override
335             public void onReceive(Context context, Intent intent) {
336                 onContentChanged();
337             }
338         };
339 
ItemLoader(Context context, String rowType)340         public ItemLoader(Context context, String rowType) {
341             super(context);
342             mRowType = rowType;
343         }
344 
345         @Override
onStartLoading()346         protected void onStartLoading() {
347             super.onStartLoading();
348             if (mConnection == null) {
349                 mConnection = new ServiceConnection() {
350                     @Override
351                     public void onServiceConnected(ComponentName name, IBinder service) {
352                         mLaunchItemsManager =
353                                 ((LaunchItemsManager.LocalBinder) service).getLaunchItemsManager();
354                         startListening();
355                     }
356 
357                     @Override
358                     public void onServiceDisconnected(ComponentName name) {
359                         stopListening();
360                         mLaunchItemsManager = null;
361                     }
362                 };
363                 getContext().bindService(new Intent(getContext(), LaunchItemsManager.class),
364                         mConnection, Context.BIND_AUTO_CREATE);
365             }
366         }
367 
startListening()368         private void startListening() {
369             switch (mRowType) {
370                 case ROW_TYPE_APPS:
371                     LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver,
372                             new IntentFilter(LaunchItemsManager.ACTION_APP_LIST_INVALIDATED));
373                     break;
374                 case ROW_TYPE_GAMES:
375                     LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver,
376                             new IntentFilter(LaunchItemsManager.ACTION_GAME_LIST_INVALIDATED));
377                     break;
378                 case ROW_TYPE_SETTINGS:
379                     LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver,
380                             new IntentFilter(LaunchItemsManager.ACTION_SETTINGS_LIST_INVALIDATED));
381                     break;
382             }
383             // Force a load to pick up any changes since we stopped listening
384             onContentChanged();
385         }
386 
stopListening()387         private void stopListening() {
388             LocalBroadcastManager.getInstance(getContext())
389                     .unregisterReceiver(mReceiver);
390         }
391 
392         @Override
onReset()393         protected void onReset() {
394             super.onReset();
395             if (mConnection != null) {
396                 getContext().unbindService(mConnection);
397                 mConnection = null;
398             }
399         }
400 
401         @Override
loadInBackground()402         public Set<LaunchItem> loadInBackground() {
403             if (mLaunchItemsManager != null) {
404                 switch (mRowType) {
405                     case ROW_TYPE_APPS:
406                         return mLaunchItemsManager.getAppItems();
407                     case ROW_TYPE_GAMES:
408                         return mLaunchItemsManager.getGameItems();
409                     case ROW_TYPE_SETTINGS:
410                         return mLaunchItemsManager.getSettingsItems();
411                     default:
412                         throw new IllegalStateException("Unknown row type");
413                 }
414             } else {
415                 return null;
416             }
417         }
418     }
419 
420     private class NotifsCountLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
421         @Override
onCreateLoader(int id, Bundle args)422         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
423             return new CursorLoader(getContext(), NotificationsContract.NOTIFS_COUNT_URI,
424                     null, null, null, null);
425         }
426 
427         @Override
onLoadFinished(Loader<Cursor> loader, Cursor data)428         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
429             if (DEBUG) {
430                 Log.d(TAG, "onLoadFinished() called with: " + "loader = [" + loader + "], data = ["
431                         + DatabaseUtils.dumpCursorToString(data) + "]");
432             }
433             if (mLaunchItemsManager != null) {
434                 mLaunchItemsManager.updateNotifsCountCursor(data);
435             }
436         }
437 
438         @Override
onLoaderReset(Loader<Cursor> loader)439         public void onLoaderReset(Loader<Cursor> loader) {
440             if (DEBUG) {
441                 Log.d(TAG, "onLoaderReset() called with: " + "loader = [" + loader + "]");
442             }
443             if (mLaunchItemsManager != null) {
444                 mLaunchItemsManager.updateNotifsCountCursor(null);
445             }
446         }
447     }
448 }
449