/* * Copyright 2015. Appsi Mobile * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.appsimobile.appsii.module.apps; import android.content.AsyncQueryHandler; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.Loader; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import com.appsimobile.appsii.AnalyticsManager; import com.appsimobile.appsii.LoaderManager; import com.appsimobile.appsii.PageController; import com.appsimobile.appsii.R; import com.appsimobile.appsii.annotation.VisibleForTesting; import com.appsimobile.appsii.compat.LauncherAppsCompat; import com.appsimobile.appsii.compat.UserHandleCompat; import com.appsimobile.appsii.dagger.AppsiInjector; import com.appsimobile.appsii.module.ToolbarScrollListener; import java.util.List; import javax.inject.Inject; /** * The controller for the apps page. Shows the app from the different folders * on the apps page. * Created by nick on 25/05/14. */ public class AppsController extends PageController implements LoaderManager.LoaderCallbacks<AppPageData>, View.OnClickListener, AppView.AppActionListener, Toolbar.OnMenuItemClickListener, AppView.TagActionListener { /** * A loader id for the all-apps list. */ @VisibleForTesting static final int APPS_LOADER_ID = 5001; /** * The list-view used to display all of the items in. */ RecyclerView mRecyclerView; /** * The adapter used to display the apps in the * list-view */ @Inject AppsAdapter mAppsAdapter; /** * The last saved amount of alpha to be applied to * the app-bar */ float mLastActionBarAlpha; /** * A query handler used to load the app tags and * tagged-apps from */ QueryHandler mQueryHandler; int mColumnCount; @Inject SharedPreferences mSharedPreferences; @Inject LauncherAppsCompat mLauncherAppsCompat; /** * A handler for the content observer to post the event into */ private Handler mHandler; /** * The toolbar for this page */ private Toolbar mToolbar; /** * The layout-manager that is being used in the recycler-view */ private GridLayoutManager mLayoutManager; /** * A helper class providing access to the bottom sheet functionality */ private BottomSheetHelper mBottomSheetHelper; public AppsController(Context context, String title) { super(context, title); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.page_apps, container, false); } @Override public void onDestroy() { super.onDestroy(); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { mRecyclerView = (RecyclerView) view.findViewById(R.id.apps_recycler_view); mBottomSheetHelper.onViewCreated(view); SharedPreferences preferences = mSharedPreferences; int defaultColumnCount = getResources().getInteger(R.integer.default_app_columns); mColumnCount = preferences.getInt("page_apps_column_count", defaultColumnCount); mLayoutManager = new GridLayoutManager(getContext(), mColumnCount); mLayoutManager.setSpanSizeLookup(new AppsSpanSizeLookup()); mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.addItemDecoration(new AppDividerDecoration(mAppsAdapter)); mToolbar = (Toolbar) view.findViewById(R.id.toolbar); mToolbar.setTitle(mTitle); setToolbarBackgroundAlpha(0); mRecyclerView.addOnScrollListener(new ToolbarScrollListener(this, mToolbar)); mAppsAdapter.setOnClickListener(this); mRecyclerView.setAdapter(mAppsAdapter); Menu menu = mToolbar.getMenu(); menu.clear(); MenuInflater menuInflater = new MenuInflater(getContext()); menuInflater.inflate(R.menu.page_apps, menu); mToolbar.setOnMenuItemClickListener(this); } @Override protected void onResume() { super.onResume(); // update the visibility and alpha of the toolbar based on the scroll. // The scroll is only available after the view-state has been restored. // // That's why it is handled in onResume; after restoring the state. updateToolbarAlpha(); } @Override protected void onUserVisible() { // when the app becomes visible to the user, track a page-view trackPageView(AnalyticsManager.CATEGORY_APPS); track(AnalyticsManager.ACTION_OPEN_PAGE, AnalyticsManager.CATEGORY_APPS); } @Override protected void onUserInvisible() { super.onUserInvisible(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AppsiInjector.inject(this); mBottomSheetHelper = new BottomSheetHelper(getContext(), this /* appController */); mAppsAdapter.setAppsActionListener(this); mAppsAdapter.setTagActionListener(this); mHandler = new Handler(); ContentResolver contentResolver = getContext().getContentResolver(); mQueryHandler = new QueryHandler(contentResolver); getLoaderManager().initLoader(APPS_LOADER_ID, null, this); } @Override protected void onDetach() { super.onDetach(); } @Override protected void onFirstLayout() { super.onFirstLayout(); } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); // when this is called, try to clean up as much memory as possible if (mAppsAdapter != null) { mAppsAdapter.onTrimMemory(level); } } @Override protected void applyToolbarColor(int color) { mToolbar.setBackgroundColor(color); } // @Override // public void hideToolbar() { // ControllerUtils.hideToolbar(this, mToolbar); // } // // @Override // public void showToolbar() { // ControllerUtils.showToolbar(this, mToolbar); // updateToolbarAlpha(); // } void updateToolbarAlpha() { View firstChild = mRecyclerView.getChildAt(0); boolean pxVisible = firstChild == null || mRecyclerView.getChildLayoutPosition(firstChild) == 0; if (pxVisible) { float pct = mAppsAdapter.getHeaderScrollPercentage(); setToolbarBackgroundAlpha(pct); } else { setToolbarBackgroundAlpha(.001f); } } @Override public Loader<AppPageData> onCreateLoader(int id, Bundle args) { if (id == APPS_LOADER_ID) { return new AppPageLoader(getContext(), mLauncherAppsCompat); } return null; } @Override public void onLoadFinished(Loader<AppPageData> loader, AppPageData data) { int id = loader.getId(); // TODO: should we notify the user somehow? if (data == null) return; if (id == APPS_LOADER_ID) { mAppsAdapter.setAppPageData(data); } AppEntry entry = mBottomSheetHelper.getBoundAppEntry(); if (entry != null) { List<TaggedApp> appliedTags = data.mTagsPerComponent.get(entry.getComponentName()); mBottomSheetHelper.updateAppliedTags(appliedTags); } mBottomSheetHelper.setAppTags(data.mAppTags); } @Override public void onLoaderReset(Loader<AppPageData> loader) { } @Override public void onClick(View v) { AppView view = (AppView) v; AppEntry app = view.getAppEntry(); LauncherAppsCompat launcherApps = mLauncherAppsCompat; launcherApps.startActivityForProfile(app.getComponentName(), UserHandleCompat.myUserHandle(), null, null); // track a launch of the app in the app history ComponentName componentName = app.getComponentName(); AppHistoryUtils.trackAppLaunch(getContext(), componentName); // track in google analytics track(AnalyticsManager.ACTION_OPEN_ITEM, AnalyticsManager.CATEGORY_APPS, app.getComponentName().flattenToShortString()); } @Override public void onEditAppliedTags(AppEntry entry, List<AppTag> allTags, List<TaggedApp> appliedTags) { mBottomSheetHelper.show(entry, appliedTags); } /** * Adds an app to a tag, this is called when the user performs an action to * add an app to a tag. */ public void onAddAppToTag(AppTag tag, AppEntry entry) { List<AppEntry> appsInTag = mAppsAdapter.getAppsInTag(tag); // determine the right index, this is 0 when the tag is empty // otherwise make it the size of the tag int idx = 0; if (appsInTag != null) { idx = appsInTag.size(); } // track the action in the ga tracker track(AnalyticsManager.ACTION_ADD_TO_TAG, AnalyticsManager.CATEGORY_APPS, entry.getComponentName().flattenToShortString()); mQueryHandler.addAppToTag(tag, entry.getComponentName(), idx); } public void onAddAppToNewTag(AppEntry entry) { Context context = getContext(); Intent intent = new Intent(context, AddTagActivity.class); intent.putExtra(AddTagActivity.EXTRA_APP_ENTRY, entry.getComponentName()); context.startActivity(intent); track(AnalyticsManager.ACTION_ADD_TAG, AnalyticsManager.CATEGORY_APPS, entry.getComponentName().flattenToShortString()); } public void onRemoveAppFromTag(TaggedApp tag) { mQueryHandler.removeAppFromTag(tag, tag.mComponentName); } @Override public boolean onMenuItemClick(MenuItem item) { int id = item.getItemId(); switch (id) { case R.id.column_count_2: track(AnalyticsManager.ACTION_CHANGE_COLUMN_COUNT, AnalyticsManager.CATEGORY_APPS); saveColumnCount(2); return true; case R.id.column_count_3: track(AnalyticsManager.ACTION_CHANGE_COLUMN_COUNT, AnalyticsManager.CATEGORY_APPS); saveColumnCount(3); return true; case R.id.column_count_4: track(AnalyticsManager.ACTION_CHANGE_COLUMN_COUNT, AnalyticsManager.CATEGORY_APPS); saveColumnCount(4); return true; case R.id.column_count_5: track(AnalyticsManager.ACTION_CHANGE_COLUMN_COUNT, AnalyticsManager.CATEGORY_APPS); saveColumnCount(5); return true; case R.id.action_edit_tags: { track(AnalyticsManager.ACTION_EDIT_TAGS, AnalyticsManager.CATEGORY_APPS); Context context = getContext(); Intent intent = new Intent(context, ReorderAppTagsActivity.class); context.startActivity(intent); return true; } } return false; } private void saveColumnCount(int columnCount) { SharedPreferences preferences = mSharedPreferences; preferences.edit(). putInt("page_apps_column_count", columnCount). apply(); mColumnCount = columnCount; mLayoutManager.setSpanCount(columnCount); mAppsAdapter.notifyDataSetChanged(); } @Override public void onEditAppTag(AppTag entry) { Intent intent = new Intent(getContext(), EditTagActivity.class); intent.putExtra(EditTagActivity.EXTRA_TAG, entry); getContext().startActivity(intent); } @Override public void onReorderApps(AppTag entry) { reorderAppsInTag(entry); } void reorderAppsInTag(AppTag appTag) { track(AnalyticsManager.ACTION_SORT_APPS, AnalyticsManager.CATEGORY_APPS); Context context = getContext(); Intent intent = new Intent(context, ReorderAppsActivity.class); intent.putExtra(ReorderAppsActivity.EXTRA_PRESELECT_TAG_ID, appTag.id); context.startActivity(intent); } @Override public void onToggleSingleRow(AppTag entry) { mQueryHandler.toggleDisplayAsList(entry); } static abstract class AbstractAppViewHolder extends RecyclerView.ViewHolder { public AbstractAppViewHolder(View itemView) { super(itemView); } abstract void bind(Object object); } protected static class QueryHandler extends AsyncQueryHandler { public QueryHandler(ContentResolver cr) { super(cr); } public void toggleDisplayAsList(AppTag appTag) { int newCount = appTag.columnCount == 1 ? 3 : 1; ContentValues values = new ContentValues(); values.put(AppsContract.TagColumns.COLUMN_COUNT, newCount); Uri contentUri = ContentUris. withAppendedId(AppsContract.TagColumns.CONTENT_URI, appTag.id); startUpdate(0, appTag, contentUri, values, null, null); } public void addAppToTag(AppTag tag, ComponentName componentName, int idx) { ContentValues values = new ContentValues(); values.put(AppsContract.TaggedAppColumns.COMPONENT_NAME, componentName.flattenToShortString()); values.put(AppsContract.TaggedAppColumns.TAG_ID, tag.id); values.put(AppsContract.TaggedAppColumns.POSITION, idx); startInsert(0, componentName, AppsContract.TaggedAppColumns.CONTENT_URI, values); } public void removeAppFromTag(TaggedApp tag, ComponentName componentName) { Uri appTagUri = ContentUris.withAppendedId( AppsContract.TaggedAppColumns.CONTENT_URI, tag.mId); startDelete(0, tag, appTagUri, null, null); } } /** * Default implementation for {GridLayoutManager.SpanSizeLookup}. Each item occupies 1 span. */ final class AppsSpanSizeLookup extends GridLayoutManager.SpanSizeLookup { public AppsSpanSizeLookup() { setSpanIndexCacheEnabled(true); } @Override public int getSpanSize(int position) { int itemType = mAppsAdapter.getItemViewType(position); if (itemType == AppsAdapter.VIEW_TYPE_PARALLAX_HEADER) return mColumnCount; if (itemType == AppsAdapter.VIEW_TYPE_HEADER) return mColumnCount; if (itemType == AppsAdapter.VIEW_TYPE_NO_RECENT_APPS) return mColumnCount; if (itemType == AppsAdapter.VIEW_TYPE_EMPTY_TAG) return mColumnCount; if (itemType == AppsAdapter.VIEW_TYPE_APP_SINGLE_ROW) return mColumnCount; return 1; } } }