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