/* * 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.home; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.Loader; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Rect; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.GridView; import android.widget.ListView; import android.widget.StackView; import com.appsimobile.appsii.AnalyticsManager; import com.appsimobile.appsii.LoaderManager; import com.appsimobile.appsii.PageController; import com.appsimobile.appsii.R; import com.appsimobile.appsii.SidebarContext; import com.appsimobile.appsii.appwidget.AppsiiAppWidgetHost; import com.appsimobile.appsii.appwidget.AppsiiAppWidgetHost.AppsiAppWidgetHostView; import com.appsimobile.appsii.dagger.AppsiInjector; import com.appsimobile.appsii.module.PermissionHelper; import com.appsimobile.appsii.module.ToolbarScrollListener; import com.appsimobile.appsii.module.home.config.HomeItemConfiguration; import com.appsimobile.appsii.module.home.config.HomeItemConfigurationHelper; import com.appsimobile.appsii.permissions.PermissionUtils; import com.crashlytics.android.Crashlytics; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; /** * Created by nick on 10/08/14. */ public class HomePageController extends PageController implements Toolbar.OnMenuItemClickListener, HomeAdapter.PermissionErrorListener, PermissionHelper.PermissionListener { // TODO implement start/stop on primary item switch private static final int HOME_LOADER_ID = 441001; final LoaderManager.LoaderCallbacks<List<HomeItem>> mHomeLoaderCallbacks = new HomeLoaderCallbacks(); final long mPageId; final SidebarContext mContext; RecyclerView mRecyclerView; GridLayoutManager mLayoutManager; @Nullable HomeAdapter mHomeAdapter; Toolbar mToolbar; boolean mLoadsDeferred; boolean mUserVisible; RecyclerViewTouchListener mRecyclerViewTouchListener; ViewGroup mPermissionOverlay; ArrayList<String> mDismissedPermissions; PendingPermissionError mPendingPermissionError; @Inject HomeItemConfigurationHelper mHomeItemConfigurationHelper; @Inject PermissionUtils mPermissionUtils; private Rect mRect; public HomePageController(Context context, long pageId, String title) { super(context, title); AppsiInjector.inject(this); mPageId = pageId; mContext = (SidebarContext) context; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { return inflater.inflate(R.layout.page_item_home, parent, false); } @Override protected void onViewDestroyed(View view) { } @Override protected void onViewCreated(View view, Bundle savedInstanceState) { mRecyclerView = (RecyclerView) view.findViewById(R.id.home_recycler); mToolbar = (Toolbar) view.findViewById(R.id.toolbar); mToolbar.setTitle(mTitle); mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.setAdapter(mHomeAdapter); mRecyclerView.addItemDecoration(new GridLayoutDecoration(getContext())); mPermissionOverlay = (ViewGroup) view.findViewById(R.id.permission_overlay); mRecyclerViewTouchListener = new RecyclerViewTouchListener(); mRecyclerView.addOnItemTouchListener(mRecyclerViewTouchListener); mRecyclerView.addOnScrollListener(new ToolbarScrollListener(this, mToolbar)); MenuInflater menuInflater = new MenuInflater(getContext()); menuInflater.inflate(R.menu.page_home, mToolbar.getMenu()); mToolbar.setOnMenuItemClickListener(this); if (mPendingPermissionError != null) { showPendingPermissionError(mPendingPermissionError); } } @Override protected void onUserVisible() { super.onUserVisible(); trackPageView(AnalyticsManager.CATEGORY_HOME); mUserVisible = true; updateHomeAdapterRunningStatus(); } private void updateHomeAdapterRunningStatus() { if (mHomeAdapter != null) { boolean started = !mLoadsDeferred && mSidebarAttached && mUserVisible; mHomeAdapter.setStarted(started); } } @Override protected void onUserInvisible() { super.onUserInvisible(); mUserVisible = false; updateHomeAdapterRunningStatus(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHomeAdapter = new HomeAdapter(getContext(), mPageId); mHomeAdapter.setPermissionErrorListener(this); mLayoutManager = new GridLayoutManager(getContext(), 12); HomeAdapter.HomeSpanSizeLookup spanSizeLookup = mHomeAdapter.getHomeSpanSizeLookup(); mLayoutManager.setSpanSizeLookup(spanSizeLookup); getLoaderManager().initLoader(HOME_LOADER_ID, null, mHomeLoaderCallbacks); if (savedInstanceState != null) { mDismissedPermissions = savedInstanceState.getStringArrayList("dismissed_permission_errors"); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putStringArrayList("dismissed_permission_errors", mDismissedPermissions); } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); if (mHomeAdapter != null) { mHomeAdapter.onTrimMemory(level); } } @Override protected void applyToolbarColor(int color) { } @Override public void setDeferLoads(boolean postponeLoads) { super.setDeferLoads(postponeLoads); mLoadsDeferred = postponeLoads; HomeViewWrapper.deferLoads(postponeLoads); updateHomeAdapterRunningStatus(); } @Override public void onSidebarAttached() { super.onSidebarAttached(); updateHomeAdapterRunningStatus(); } @Override public void onSidebarDetached() { super.onSidebarDetached(); updateHomeAdapterRunningStatus(); } @Override public int shouldClose(Bundle state) { if (mHomeAdapter == null) return PageController.CLOSE_ACTION_AUTO_CLOSE; int lastX = mRecyclerViewTouchListener.mTouchDownX; int lastY = mRecyclerViewTouchListener.mTouchDownY; AppWidgetHostView hostView = findAppWidgetHostView(mRecyclerView, lastX, lastY); if (hostView != null) { View clicked = findAppWidgetButton(mRecyclerView, lastX, lastY); if (clicked != null && clicked.getId() != View.NO_ID) { return doShouldClose(hostView, clicked, state); } } return super.shouldClose(state); } @Override public void rememberCloseAction(Bundle state, int action) { if (action == CLOSE_ACTION_AUTO_CLOSE || action == CLOSE_ACTION_KEEP_OPEN) { long homeItemId = state.getLong("home_item_id", -1L); String viewName = state.getString("view_name"); HomeItemConfiguration helper = mHomeItemConfigurationHelper; if (action == CLOSE_ACTION_AUTO_CLOSE) { String closeButtonNames = helper.getProperty(homeItemId, "always_close_buttons", null); if (closeButtonNames == null) { closeButtonNames = viewName; } else { closeButtonNames += "," + viewName; } helper.updateProperty(homeItemId, "always_close_buttons", closeButtonNames); } else { String keepOpenButtonNames = helper.getProperty(homeItemId, "keep_open_buttons", null); if (keepOpenButtonNames == null) { keepOpenButtonNames = viewName; } else { keepOpenButtonNames += "," + viewName; } helper.updateProperty(homeItemId, "keep_open_buttons", keepOpenButtonNames); } } } private boolean isScrollableWidgetView(View view) { return view instanceof GridView || view instanceof ListView || view instanceof StackView; } @Nullable View findScrollableWidgetView(ViewGroup container, int x, int y) { if (mRect == null) { mRect = new Rect(); } final Rect r = mRect; final int count = container.getChildCount(); final int scrolledX = x + container.getScrollX(); final int scrolledY = y + container.getScrollY(); //final View ignoredDropTarget = (View) mIgnoredDropTarget; for (int i = count - 1; i >= 0; i--) { final View child = container.getChildAt(i); if (child.getVisibility() == View.VISIBLE) { child.getHitRect(r); if (r.contains(scrolledX, scrolledY)) { View target = null; if (child instanceof ViewGroup) { x = scrolledX - child.getLeft(); y = scrolledY - child.getTop(); target = findScrollableWidgetView((ViewGroup) child, x, y); } if (target == null) { if (isScrollableWidgetView(child)) { return child; } } else { if (isScrollableWidgetView(target)) { return target; } } } } } return null; } @Nullable AppsiAppWidgetHostView findAppWidgetHostView(ViewGroup container, int x, int y) { if (mRect == null) { mRect = new Rect(); } final Rect r = mRect; final int count = container.getChildCount(); final int scrolledX = x + container.getScrollX(); final int scrolledY = y + container.getScrollY(); //final View ignoredDropTarget = (View) mIgnoredDropTarget; for (int i = count - 1; i >= 0; i--) { final View child = container.getChildAt(i); if (child.getVisibility() == View.VISIBLE) { child.getHitRect(r); if (r.contains(scrolledX, scrolledY)) { if (child instanceof AppWidgetHostView) { return (AppsiAppWidgetHostView) child; } if (child instanceof ViewGroup) { x = scrolledX - child.getLeft(); y = scrolledY - child.getTop(); return findAppWidgetHostView((ViewGroup) child, x, y); } } } } return null; } @Nullable View findAppWidgetButton(ViewGroup container, int x, int y) { if (mRect == null) { mRect = new Rect(); } final Rect r = mRect; final int count = container.getChildCount(); final int scrolledX = x + container.getScrollX(); final int scrolledY = y + container.getScrollY(); //final View ignoredDropTarget = (View) mIgnoredDropTarget; for (int i = count - 1; i >= 0; i--) { final View child = container.getChildAt(i); if (child.getVisibility() == View.VISIBLE) { child.getHitRect(r); if (r.contains(scrolledX, scrolledY)) { if (child instanceof ViewGroup) { x = scrolledX - child.getLeft(); y = scrolledY - child.getTop(); View result = findAppWidgetButton((ViewGroup) child, x, y); if (result != null) return result; } if (child.getId() != View.NO_ID && child.isClickable()) { return child; } } } } return null; } void onHomeItemsLoaded(List<HomeItem> data) { // clear any permission errors that might be pending if (mPermissionOverlay != null) { mPermissionOverlay.removeAllViews(); } if (mHomeAdapter == null) { Crashlytics.logException(new NullPointerException("adapter not initialized?!?")); return; } mHomeAdapter.setHomeItems(data); } @Override public boolean onMenuItemClick(MenuItem menuItem) { int itemId = menuItem.getItemId(); if (itemId == R.id.action_edit_layout) { showLayoutEditor(); return true; } return false; } private void showLayoutEditor() { Intent intent = new Intent(getContext(), HomeEditorActivity.class); intent.putExtra(HomeEditorActivity.EXTRA_PAGE_ID, mPageId); intent.putExtra(HomeEditorActivity.EXTRA_PAGE_TITLE, mTitle); getContext().startActivity(intent); } private int doShouldClose(AppWidgetHostView hostView, View clicked, Bundle state) { if (mHomeAdapter == null) return CLOSE_ACTION_DONT_KNOW; state.clear(); int appWidgetId = hostView.getAppWidgetId(); String viewName = getViewName(hostView.getAppWidgetInfo(), clicked); HomeItem homeItem = mHomeAdapter.getHomeItemForAppWidgetId(appWidgetId); HomeItemConfiguration helper = mHomeItemConfigurationHelper; String closeButtonNames = helper.getProperty(homeItem.mId, "always_close_buttons", null); String keepOpenButtonNames = helper.getProperty(homeItem.mId, "keep_open_buttons", null); String[] closeArr = splitArray(closeButtonNames); String[] openArr = splitArray(keepOpenButtonNames); if (arrayContains(viewName, closeArr)) return CLOSE_ACTION_AUTO_CLOSE; if (arrayContains(viewName, openArr)) return CLOSE_ACTION_KEEP_OPEN; state.putLong("home_item_id", homeItem.mId); state.putString("view_name", viewName); return CLOSE_ACTION_ASK; } <T> boolean arrayContains(T t, T[] ts) { if (ts == null) return false; int N = ts.length; for (int i = 0; i < N; i++) { if (t.equals(ts[i])) return true; } return false; } private String[] splitArray(String s) { return s == null ? null : s.split(","); } @Nullable String getViewName(AppWidgetProviderInfo info, View view) { ComponentName provider = info.provider; Resources resources; try { PackageManager packageManager = mContext.getPackageManager(); resources = packageManager.getResourcesForApplication(provider.getPackageName()); } catch (PackageManager.NameNotFoundException e) { Log.wtf("Home", "App not installed??", e); return null; } return resources.getResourceEntryName(view.getId()); } @Override public void onPermissionDenied(String permission, String id, @StringRes int textResId) { if (mPermissionUtils.shouldShowPermissionError(mContext, id)) { if (mPermissionOverlay == null) { if (mPendingPermissionError == null) { mPendingPermissionError = new PendingPermissionError(permission, id, textResId); } } else { showPermissionError(permission, id, textResId); } } } private void showPendingPermissionError(PendingPermissionError pendingPermissionError) { showPermissionError(pendingPermissionError.mPermission, pendingPermissionError.mId, pendingPermissionError.mTextResId); } private void showPermissionError(String permission, String id, @StringRes int textResId) { PermissionHelper permissionHelper = new PermissionHelper(textResId, true, this, permission); mPermissionOverlay.setTag(id); permissionHelper.show(mPermissionOverlay); } @Override public void onAccepted(PermissionHelper permissionHelper) { Intent intent = mPermissionUtils. buildRequestPermissionsIntent(mContext, 1, permissionHelper.getPermissions()); mContext.startActivity(intent); } @Override public void onCancelled(PermissionHelper permissionHelper, boolean dontShowAgain) { if (dontShowAgain) { String id = (String) mPermissionOverlay.getTag(); mPermissionUtils.setDontShowPermissionAgain(mContext, id); } } static class PendingPermissionError { final String mPermission; final String mId; @StringRes final int mTextResId; PendingPermissionError(String permission, String id, int textResId) { mPermission = permission; mId = id; mTextResId = textResId; } } class HomeLoaderCallbacks implements LoaderManager.LoaderCallbacks<List<HomeItem>> { @Override public Loader<List<HomeItem>> onCreateLoader(int id, Bundle args) { return new HomeLoader(getContext()); } @Override public void onLoadFinished(Loader<List<HomeItem>> loader, List<HomeItem> data) { onHomeItemsLoaded(data); } @Override public void onLoaderReset(Loader<List<HomeItem>> loader) { } } private class RecyclerViewTouchListener implements RecyclerView.OnItemTouchListener { final Rect mRect = new Rect(); final int mTouchSlop; AppsiiAppWidgetHost.CapturedEventQueue mEventQueue; int mTouchDownX; int mTouchDownY; AppWidgetHostView mHostView; RecyclerViewTouchListener() { mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop() / 2; } @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { int action = e.getAction(); if (action == MotionEvent.ACTION_DOWN) { // Clear previous state, in case we never received the up/cancel. // According to the source in ViewGroup this may happen (see below). // // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. if (mEventQueue != null) { mEventQueue.release(); mEventQueue = null; } mHostView = null; // remember initial touchdown position int x = (int) e.getX(); int y = (int) e.getY(); mTouchDownX = x; mTouchDownY = y; // Find out if an app-widget-host-view is below the given x/y AppsiAppWidgetHostView hostView = findAppWidgetHostView(rv, x, y); if (hostView == null) { // if not the touch down was outside the widget area; ignore the event mEventQueue = null; return false; } // if it was on an app-widget, find out if it was on a scrollable widget // and if so, get it's capture queue. // The capture queue is a special queue we can send events to. The widget // will ignore all touch events coming from other sources than the queue View scrollableWidgetView = findScrollableWidgetView(rv, x, y); if (scrollableWidgetView != null) { mEventQueue = hostView.captureEventQueue(); } if (mEventQueue != null) { mHostView = hostView; dispatchTouchEvent(rv, e, mHostView); } else { mHostView = null; } return false; } else if (mEventQueue != null && action == MotionEvent.ACTION_MOVE) { Log.i("Home", "Move on hostview; starting capture"); return true; } else if (mEventQueue != null && (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP)) { Log.i("Home", "Up/cancel on hostview (dispatching); finishing capture"); dispatchTouchEvent(rv, e, mHostView); mEventQueue.release(); mEventQueue = null; mHostView = null; return false; } return mEventQueue != null; } @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { dispatchTouchEvent(rv, e, mHostView); int action = e.getAction(); // clear the current queue on cancel or up if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { if (mEventQueue != null) { mEventQueue.release(); mEventQueue = null; } else { // This can't really be null at this point Crashlytics.logException(new NullPointerException("Should not happen")); } } } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } private void dispatchTouchEvent(RecyclerView rv, MotionEvent e, final AppWidgetHostView delegateView) { float ox = (int) e.getX(); float oy = (int) e.getY(); // Offset event coordinates to be inside the target view mRect.set((int) ox, (int) oy, 0, 0); rv.offsetRectIntoDescendantCoords(delegateView, mRect); int x = mRect.left; int y = mRect.top; final MotionEvent copy = MotionEvent.obtain(e); // deliver the event copy.setLocation(x, y); mEventQueue.dispatchTouchEvent(copy); } } }