/*
* 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.ComponentName;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.LongSparseArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.PopupMenu;
import android.widget.TextView;
import com.appsimobile.appsii.ExpandCollapseDrawable;
import com.appsimobile.appsii.R;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
/**
* Created by nick on 09/01/15.
*/
class AppsAdapter extends RecyclerView.Adapter<AppsController.AbstractAppViewHolder> {
static final int VIEW_TYPE_PARALLAX_HEADER = R.layout.page_apps_px_header;
static final int VIEW_TYPE_HEADER = R.layout.list_item_header_collapsible_with_options;
static final int VIEW_TYPE_APP = R.layout.grid_item_app;
static final int VIEW_TYPE_APP_SINGLE_ROW = R.layout.list_item_app;
static final int VIEW_TYPE_NO_RECENT_APPS = R.layout.list_item_no_recent_apps;
static final int VIEW_TYPE_EMPTY_TAG = R.layout.list_item_empty_tag;
final LongSparseArray<Boolean> mExpandedTags = new LongSparseArray<>();
/**
* A list of items that are currently showing in the list/grid
*/
private final List<Object> mVisibleItems = new ArrayList<>();
AppView.AppActionListener mAppActionListener;
AppView.TagActionListener mTagActionListener;
AppPageData mData;
View.OnClickListener mOnClickListener;
private View mParallaxView;
@Inject
AppsAdapter() {
mVisibleItems.add(null);
setHasStableIds(true);
}
public void setAppPageData(AppPageData data) {
boolean firstRun = mVisibleItems.size() <= 1;
// TODO: make it recycle existing loaded icons
mData = data;
mVisibleItems.clear();
// parallax-view
mVisibleItems.add(null);
List<AppTag> mAppTags = mData.mAppTags;
int N = mAppTags.size();
for (int i = 0; i < N; i++) {
AppTag tag = mAppTags.get(i);
if (firstRun) {
mExpandedTags.put(tag.id, tag.defaultExpanded ? Boolean.TRUE : Boolean.FALSE);
}
boolean expanded = mExpandedTags.get(tag.id, Boolean.FALSE);
mVisibleItems.add(tag);
if (expanded) {
List<AppEntry> appsInTag = getAppsInTag(tag);
boolean empty = appsInTag == null || appsInTag.isEmpty();
if (empty && tag.tagType == AppsContract.TagColumns.TAG_TYPE_USER) {
mVisibleItems.add(new EmptyTagAppsItem());
} else if (empty && tag.tagType == AppsContract.TagColumns.TAG_TYPE_RECENT) {
mVisibleItems.add(new NoRecentAppsItem());
} else if (!empty) {
mVisibleItems.addAll(appsInTag);
}
}
}
notifyDataSetChanged();
}
@Nullable
public List<AppEntry> getAppsInTag(AppTag tag) {
return mData.mAppsPerTag.get(tag.id);
}
@Override
public AppsController.AbstractAppViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_PARALLAX_HEADER:
return onCreateParallaxViewHolder(parent);
case VIEW_TYPE_HEADER:
return onCreateGroupViewHolder(parent);
case VIEW_TYPE_NO_RECENT_APPS:
return onCreateEmptyRecentsViewHolder(parent);
case VIEW_TYPE_EMPTY_TAG:
return onEmptyTagViewHolder(parent);
case VIEW_TYPE_APP:
case VIEW_TYPE_APP_SINGLE_ROW:
return onCreateAppViewHolder(parent, viewType);
}
return null;
}
protected AppsController.AbstractAppViewHolder onCreateParallaxViewHolder(ViewGroup parent) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View result = layoutInflater.inflate(R.layout.page_apps_px_header, parent, false);
mParallaxView = result;
return new ParallaxHeaderHolder(result);
}
public AppHeaderHolder onCreateGroupViewHolder(ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view =
inflater.inflate(R.layout.list_item_header_collapsible_with_options, parent, false);
return new AppHeaderHolder(view);
}
public NoRecentAppsHolder onCreateEmptyRecentsViewHolder(ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.list_item_no_recent_apps, parent, false);
return new NoRecentAppsHolder(view);
}
public NoRecentAppsHolder onEmptyTagViewHolder(ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.list_item_empty_tag, parent, false);
return new NoRecentAppsHolder(view);
}
public AppHolder onCreateAppViewHolder(ViewGroup parent, int viewType) {
View appView = createAppView(parent, viewType);
return new AppHolder(appView);
}
public View createAppView(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
return inflater.inflate(viewType, parent, false);
}
@Override
public void onBindViewHolder(AppsController.AbstractAppViewHolder holder, int position) {
Object item = mVisibleItems.get(position);
holder.bind(item);
}
@Override
public int getItemViewType(int position) {
if (position == 0) return VIEW_TYPE_PARALLAX_HEADER;
if (position >= mVisibleItems.size()) return VIEW_TYPE_APP;
Object item = mVisibleItems.get(position);
if (item instanceof AppTag) return VIEW_TYPE_HEADER;
if (item instanceof NoRecentAppsItem) return VIEW_TYPE_NO_RECENT_APPS;
if (item instanceof EmptyTagAppsItem) return VIEW_TYPE_EMPTY_TAG;
if (item instanceof AppEntry) {
AppTag appTagWrapper = tagForPosition(position);
if (appTagWrapper != null && appTagWrapper.columnCount == 1) {
return VIEW_TYPE_APP_SINGLE_ROW;
}
return VIEW_TYPE_APP;
}
throw new IllegalStateException("unknown item type at position: " + position +
" class: " + item.getClass().getSimpleName());
}
@Override
public long getItemId(int position) {
Object item = mVisibleItems.get(position);
if (item == null) return RecyclerView.NO_ID;
long result = getItemViewType(position);
result = result << 32;
if (item instanceof AppTag) return result ^ ((AppTag) item).id;
if (item instanceof NoRecentAppsItem) return result ^ position;
if (item instanceof EmptyTagAppsItem) return result ^ position * 2;
if (item instanceof ResolveInfoAppEntry) {
AppTag tag = tagForPosition(position);
if (tag != null) {
return result ^ ((ResolveInfoAppEntry) item).getComponentName().hashCode() ^ tag.id;
} else {
return result ^ ((ResolveInfoAppEntry) item).getComponentName().hashCode();
}
}
return result ^ item.hashCode();
}
@Nullable
AppTag tagForPosition(int position) {
Object target = mVisibleItems.get(position);
while (position >= 0) {
if (target instanceof AppTag) return (AppTag) target;
--position;
target = mVisibleItems.get(position);
}
return null;
}
@Override
public int getItemCount() {
return mVisibleItems.size();
}
public void setOnClickListener(View.OnClickListener onClickListener) {
mOnClickListener = onClickListener;
}
public void onTrimMemory(int level) {
if (mData == null || mData.mAllApps == null) return;
List<AppEntry> mAllApps = mData.mAllApps;
int N = mAllApps.size();
for (int i = 0; i < N; i++) {
AppEntry tag = mAllApps.get(i);
tag.trimMemory();
}
}
void onCollapse(int position) {
AppTag appTag = (AppTag) mVisibleItems.get(position);
List<AppEntry> appsInTag = getAppsInTag(appTag);
int count = appsInTag == null ? 0 : appsInTag.size();
if (count == 0) {
if (appTag.tagType == AppsContract.TagColumns.TAG_TYPE_USER ||
appTag.tagType == AppsContract.TagColumns.TAG_TYPE_RECENT) {
count = 1;
}
}
int firstPos = position + 1;
for (int i = firstPos + count - 1; i >= firstPos; i--) {
mVisibleItems.remove(i);
}
notifyItemRangeRemoved(firstPos, count);
}
void onExpand(int position) {
AppTag tag = (AppTag) mVisibleItems.get(position);
List<AppEntry> appsInTag = getAppsInTag(tag);
int count = appsInTag == null ? 0 : appsInTag.size();
if (count == 0 && tag.tagType == AppsContract.TagColumns.TAG_TYPE_USER) {
mVisibleItems.add(position + 1, new EmptyTagAppsItem());
count = 1;
} else if (count == 0 && tag.tagType == AppsContract.TagColumns.TAG_TYPE_RECENT) {
mVisibleItems.add(position + 1, new NoRecentAppsItem());
count = 1;
} else {
mVisibleItems.addAll(position + 1, appsInTag);
}
notifyItemRangeInserted(position + 1, count);
}
public float getHeaderScrollPercentage() {
if (mParallaxView == null) return 1f;
if (!mParallaxView.isShown()) return 1f;
float top = mParallaxView.getTop();
if (top > 0) top = 0;
return top / mParallaxView.getHeight();
}
public void setAppsActionListener(AppView.AppActionListener l) {
mAppActionListener = l;
}
public void setTagActionListener(AppView.TagActionListener l) {
mTagActionListener = l;
}
static class NoRecentAppsItem implements AppEntry {
@Override
public ApplicationInfo getApplicationInfo() {
return null;
}
@Override
public ComponentName getComponentName() {
return null;
}
@Override
public CharSequence getLabel() {
return null;
}
@Override
public Drawable getIconIfReady() {
return null;
}
@Override
public Drawable getIcon(PackageManager packageManager) {
return null;
}
@Override
public void trimMemory() {
}
}
static class EmptyTagAppsItem implements AppEntry {
@Override
public ApplicationInfo getApplicationInfo() {
return null;
}
@Override
public ComponentName getComponentName() {
return null;
}
@Override
public CharSequence getLabel() {
return null;
}
@Override
public Drawable getIconIfReady() {
return null;
}
@Override
public Drawable getIcon(PackageManager packageManager) {
return null;
}
@Override
public void trimMemory() {
}
}
static class ParallaxHeaderHolder extends AppsController.AbstractAppViewHolder {
public ParallaxHeaderHolder(View itemView) {
super(itemView);
}
@Override
void bind(Object object) {
}
}
static class OverflowTouchDelegate extends TouchDelegate {
final int mMinHeight;
final View mDelegateView;
final private Rect mBounds;
final private Rect mSlopBounds;
private final int mSlop;
boolean mDelegateTargeted;
public OverflowTouchDelegate(View delegateView) {
super(new Rect(), delegateView);
mBounds = new Rect();
mSlopBounds = new Rect();
mDelegateView = delegateView;
mMinHeight = (int) (delegateView.getResources().getDisplayMetrics().density * 48);
mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
}
/**
* Will forward touch events to the delegate view if the event is within the bounds
* specified in the constructor.
*
* @param event The touch event to forward
*
* @return True if the event was forwarded to the delegate, false otherwise.
*/
public boolean onTouchEvent(@NonNull MotionEvent event) {
updateBounds();
int x = (int) event.getX();
int y = (int) event.getY();
boolean sendToDelegate = false;
boolean hit = true;
boolean handled = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Rect bounds = mBounds;
if (bounds.contains(x, y)) {
mDelegateTargeted = true;
sendToDelegate = true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_MOVE:
sendToDelegate = mDelegateTargeted;
if (sendToDelegate) {
Rect slopBounds = mSlopBounds;
if (!slopBounds.contains(x, y)) {
hit = false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
sendToDelegate = mDelegateTargeted;
mDelegateTargeted = false;
break;
}
if (sendToDelegate) {
final View delegateView = mDelegateView;
if (hit) {
// Offset event coordinates to be inside the target view
event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
} else {
// Offset event coordinates to be outside the target view (in case it does
// something like tracking pressed state)
int slop = mSlop;
event.setLocation(-(slop * 2), -(slop * 2));
}
handled = delegateView.dispatchTouchEvent(event);
}
return handled;
}
void updateBounds() {
ViewParent parent = mDelegateView.getParent();
mBounds.set(0, 0, mDelegateView.getWidth(), mDelegateView.getHeight());
if (mBounds.height() < mMinHeight) {
mBounds.bottom = mBounds.top + mMinHeight;
}
mSlopBounds.set(mBounds);
mSlopBounds.inset(-mSlop, -mSlop);
parent.getChildVisibleRect(mDelegateView, mBounds, null);
}
}
class NoRecentAppsHolder extends AppsController.AbstractAppViewHolder {
public NoRecentAppsHolder(View itemView) {
super(itemView);
}
@Override
void bind(Object object) {
}
}
class AppHeaderHolder extends AppsController.AbstractAppViewHolder
implements View.OnClickListener, PopupMenu.OnMenuItemClickListener {
final TextView mTextView;
final View mHeaderOverflow;
AppTag mAppTag;
public AppHeaderHolder(View itemView) {
super(itemView);
TypedArray a = itemView.getContext().getTheme().obtainStyledAttributes(new int[]{
R.attr.colorPrimary,
R.attr.appsiMenuItemTint,
R.attr.colorAccent,
});
int accentColor = a.getColor(2, Color.DKGRAY);
a.recycle();
Resources res = itemView.getResources();
mTextView = (TextView) itemView.findViewById(R.id.app_header_expander);
mHeaderOverflow = itemView.findViewById(R.id.header_overflow);
int dp48 = (int) (res.getDisplayMetrics().density * 24);
Drawable drawable = new ExpandCollapseDrawable(res, accentColor);
drawable.setBounds(0, 0, dp48, dp48);
boolean isLtr = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) ==
View.LAYOUT_DIRECTION_LTR;
if (isLtr) {
mTextView.setCompoundDrawables(null, null, drawable, null);
} else {
mTextView.setCompoundDrawables(drawable, null, null, null);
}
mTextView.setOnClickListener(this);
mHeaderOverflow.setOnClickListener(this);
itemView.setTouchDelegate(new OverflowTouchDelegate(mHeaderOverflow));
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.app_header_expander) {
onHeaderTitleClicked();
} else if (id == R.id.header_overflow) {
onHeaderOverflowClicked();
}
}
private void onHeaderTitleClicked() {
// get and toggle
boolean expanded = !mExpandedTags.get(mAppTag.id, Boolean.FALSE);
mExpandedTags.put(mAppTag.id, expanded);
if (expanded) {
onExpand(getPosition());
} else {
onCollapse(getPosition());
}
setExpanded(expanded, true);
}
private void onHeaderOverflowClicked() {
boolean editable =
mAppTag.tagType == AppsContract.TagColumns.TAG_TYPE_USER;
boolean showingAsList = mAppTag.columnCount == 1;
PopupMenu popupMenu = new PopupMenu(mHeaderOverflow.getContext(), mHeaderOverflow);
MenuInflater menuInflater = popupMenu.getMenuInflater();
Menu menu = popupMenu.getMenu();
menuInflater.inflate(R.menu.page_apps_tag, menu);
menu.findItem(R.id.action_sort_apps).setVisible(editable);
menu.findItem(R.id.action_toggle_list).setChecked(showingAsList);
popupMenu.setOnMenuItemClickListener(this);
popupMenu.show();
}
void setExpanded(boolean expanded, boolean animate) {
boolean isLtr = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) ==
View.LAYOUT_DIRECTION_LTR;
Drawable[] drawables = mTextView.getCompoundDrawables();
if (isLtr) {
((ExpandCollapseDrawable) drawables[2]).setExpanded(expanded, animate);
} else {
((ExpandCollapseDrawable) drawables[0]).setExpanded(expanded, animate);
}
}
@Override
public boolean onMenuItemClick(MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.action_sort_apps:
mTagActionListener.onReorderApps(mAppTag);
return true;
case R.id.action_toggle_list:
mTagActionListener.onToggleSingleRow(mAppTag);
return true;
case R.id.action_rename_tag:
mTagActionListener.onEditAppTag(mAppTag);
return true;
}
return false;
}
@Override
void bind(Object object) {
mAppTag = (AppTag) object;
String title = mAppTag.title;
mTextView.setText(title);
boolean expanded = mExpandedTags.get(mAppTag.id, Boolean.FALSE);
setExpanded(expanded, false);
}
}
class AppHolder extends AppsController.AbstractAppViewHolder {
public AppHolder(View itemView) {
super(itemView);
}
@Override
void bind(Object object) {
AppEntry item = (AppEntry) object;
AppView appView = (AppView) itemView;
List<TaggedApp> tags;
if (item != null) {
appView.setOnClickListener(mOnClickListener);
ComponentName cn = item.getComponentName();
tags = mData.mTagsPerComponent.get(cn);
} else {
// remove the listener. Even if no child is set, the view
// may still be added.
appView.setOnClickListener(null);
tags = null;
}
appView.setAppActionListener(mAppActionListener);
appView.bind(item, tags);
}
}
}