/* * 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; import android.content.Context; import android.content.Loader; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import com.appsimobile.annotation.KeepName; import com.appsimobile.appsii.annotation.VisibleForTesting; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; /** * A simple wrapper for a page, used to save instance state of the * view and persist it's state while the sidebar is closed. * Functions mostly similar to a normal fragment */ public abstract class PageController implements ViewTreeObserver.OnGlobalLayoutListener { protected static final int CLOSE_ACTION_AUTO_CLOSE = 1; protected static final int CLOSE_ACTION_KEEP_OPEN = 2; protected static final int CLOSE_ACTION_DONT_KNOW = 3; protected static final int CLOSE_ACTION_ASK = 4; protected final int mPrimaryColor; protected final int mAccentColor; protected final int mDefaultTintColor; protected final String mTitle; final ArrayList<Runnable> mDeferredRunnables = new ArrayList<>(8); private final SidebarContext mContext; protected boolean mSidebarAttached; @VisibleForTesting boolean mUserVisible; LoaderManager mLoaderManager; boolean mLoadsDeferred; SearchRequestListener mSearchRequestListener; private HotspotPageEntry mPage; private View mView; private SparseArray<Parcelable> mSavedHierarchyState = new SparseArray<>(); private Bundle mClientSaveState = new Bundle(); public PageController(Context context, String title) { mContext = (SidebarContext) context; mTitle = title; TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{ R.attr.colorPrimary, R.attr.appsiMenuItemTint, R.attr.colorAccent, }); mPrimaryColor = a.getColor(0, Color.WHITE); mDefaultTintColor = a.getColor(1, Color.BLACK); mAccentColor = a.getColor(2, Color.BLUE); a.recycle(); } public HotspotPageEntry getPage() { return mPage; } public void setPage(HotspotPageEntry page) { mPage = page; } public LoaderManager getLoaderManager() { if (mLoaderManager == null) { mLoaderManager = new LoaderManagerWrapper(mContext.getLoaderManager()); } return mLoaderManager; } public int getContentWidth() { return mContext.getContentWidth(); } public Context getContext() { return mContext; } public void track(String action, String category) { mContext.track(action, category); } public void trackPageView(String page) { mContext.trackPageView(page); } public void track(String action, String category, String label) { mContext.track(action, category, label); } public Resources getResources() { return mContext.getResources(); } /** * Creates the view, calls onCreateView with the saved instance state */ public final View performCreateView(LayoutInflater inflater, ViewGroup container) { return onCreateView(inflater, container, mClientSaveState); } public abstract View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState); public final void performDestroy() { onDestroy(); onViewDestroyed(mView); mView = null; } public void onDestroy() { } /** * Called after onDetach in case the adapter decides to * destroy the view */ protected void onViewDestroyed(View view) { } public final void performViewCreated(View view) { mView = view; ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); // Note: This happens in test cases. if (viewTreeObserver != null) { viewTreeObserver.addOnGlobalLayoutListener(this); } onViewCreated(view, mClientSaveState); postViewCreated(view); } protected abstract void onViewCreated(View view, Bundle savedInstanceState); private void postViewCreated(View view) { StatusbarUnderlay underlay = (StatusbarUnderlay) view.findViewById(R.id.underlay); if (underlay != null) { underlay.setSidebarContext(mContext); } } public final void performStart() { onStart(); } protected void onStart() { } public final void performPause() { onPause(); } protected void onPause() { } public final void performStop() { onStop(); } protected void onStop() { } public final void performRestoreInstanceState() { restoreViewState(); } private void restoreViewState() { if (mSavedHierarchyState != null) { mView.restoreHierarchyState(mSavedHierarchyState); } } public final void performResume() { onResume(); } protected void onResume() { } /** * This page has become the visible page in the adapter */ protected void onUserVisible() { } /** * This page is no longer the visible page in the adapter. */ protected void onUserInvisible() { } @VisibleForTesting public final void performCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { restoreInstanceState(savedInstanceState); } onCreate(mClientSaveState); } private void restoreInstanceState(@NonNull Bundle savedInstanceState) { mSavedHierarchyState = savedInstanceState.getSparseParcelableArray("viewState"); mClientSaveState = savedInstanceState.getBundle("clientState"); } protected void onCreate(Bundle savedInstanceState) { } final void performSaveInstanceState(Bundle outState) { saveViewState(); outState.putSparseParcelableArray("viewState", mSavedHierarchyState); mClientSaveState.clear(); onSaveInstanceState(mClientSaveState); outState.putBundle("clientState", mClientSaveState); } private void saveViewState() { mSavedHierarchyState.clear(); mView.saveHierarchyState(mSavedHierarchyState); } /** * Client hook to save the view state. Will be called before destroying * the view. Will be provided in onCreate, onCreateView and onViewCreated */ protected void onSaveInstanceState(Bundle outState) { } public final void performOnAttach() { onAttach(); } protected void onAttach() { } public final void performOnDetach() { onDetach(); } protected void onDetach() { } @Override public void onGlobalLayout() { onFirstLayout(); // Can mView ever be null here? ViewTreeObserver viewTreeObserver = mView == null ? null : mView.getViewTreeObserver(); if (viewTreeObserver != null) { viewTreeObserver.removeOnGlobalLayoutListener(this); } } protected void onFirstLayout() { } public final View getView() { return mView; } public boolean isUserVisible() { return mUserVisible; } public void setUserVisible(boolean userVisible) { mUserVisible = userVisible; } public void onTrimMemory(int level) { } @KeepName public void setToolbarBackgroundAlpha(float visiblePct) { // 0 = completely hidden // 1 = completely visible if (visiblePct == 0) { applyToolbarColor(Color.TRANSPARENT); } else { int color = evaluate(visiblePct, mPrimaryColor & 0x00FFFFFF, mPrimaryColor); applyToolbarColor(color); } } protected abstract void applyToolbarColor(int color); public int evaluate(float fraction, Object startValue, Object endValue) { int startInt = (Integer) startValue; int startA = (startInt >> 24) & 0xff; int startR = (startInt >> 16) & 0xff; int startG = (startInt >> 8) & 0xff; int startB = startInt & 0xff; int endInt = (Integer) endValue; int endA = (endInt >> 24) & 0xff; int endR = (endInt >> 16) & 0xff; int endG = (endInt >> 8) & 0xff; int endB = endInt & 0xff; return (startA + (int) (fraction * (endA - startA))) << 24 | (startR + (int) (fraction * (endR - startR))) << 16 | (startG + (int) (fraction * (endG - startG))) << 8 | (startB + (int) (fraction * (endB - startB))); } /** * Can be implemented by clients to indicate certain desired behaviors * to the sidebar. For example to hide the background */ public int getFlags() { return 0; } @CallSuper public void setDeferLoads(boolean deferLoads) { Log.d("PageController", "setting deferloads: " + deferLoads); mLoadsDeferred = deferLoads; if (!deferLoads) { for (int i = 0; i < mDeferredRunnables.size(); i++) { Runnable r = mDeferredRunnables.get(i); r.run(); } mDeferredRunnables.clear(); } } public void onSidebarAttached() { mSidebarAttached = true; } public void onSidebarDetached() { mSidebarAttached = false; } /** * Called to identify if the sidebar should close. Expensive state can be saved in the bundle */ public int shouldClose(Bundle state) { return SidebarPagerAdapter.CLOSE_ACTION_DONT_KNOW; } /** * Called when the user would like to remember a action for a specific button. * The provided state is the same state provided to the shouldClose call. */ public void rememberCloseAction(Bundle state, int action) { } protected <T> LoaderManager.LoaderCallbacks<T> wrapCallback( LoaderManager.LoaderCallbacks<T> cb) { return new CallbackWrapper<>(cb); } public void requestSearch() { if (mSearchRequestListener != null) { mSearchRequestListener.onSearchRequested(); } } public void setSearchRequestListener( SearchRequestListener searchRequestListener) { mSearchRequestListener = searchRequestListener; } public interface SearchRequestListener { void onSearchRequested(); } class LoaderManagerWrapper extends LoaderManager { LoaderManager mWrapped; public LoaderManagerWrapper(LoaderManager wrap) { mWrapped = wrap; } @Override public <D> Loader<D> initLoader(int id, Bundle args, LoaderCallbacks<D> callback) { return mWrapped.initLoader(id, args, wrapCallback(callback)); } private <D> LoaderCallbacks<D> wrapCallback(LoaderCallbacks<D> callback) { return PageController.this.wrapCallback(callback); } @Override public <D> Loader<D> restartLoader(int id, Bundle args, LoaderCallbacks<D> callback) { return mWrapped.restartLoader(id, args, wrapCallback(callback)); } @Override public void destroyLoader(int id) { mWrapped.destroyLoader(id); } @Override public <D> Loader<D> getLoader(int id) { return mWrapped.getLoader(id); } @Override public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { mWrapped.dump(prefix, fd, writer, args); } @Override public boolean hasRunningLoaders() { return mWrapped.hasRunningLoaders(); } } class CallbackWrapper<T> implements LoaderManager.LoaderCallbacks<T>, Runnable { final LoaderManager.LoaderCallbacks<T> mLoaderCallbacks; Pair<Loader<T>, T> mResult; public CallbackWrapper(LoaderManager.LoaderCallbacks<T> cb) { mLoaderCallbacks = cb; } @Override public Loader<T> onCreateLoader(int id, Bundle args) { return mLoaderCallbacks.onCreateLoader(id, args); } @Override public void onLoadFinished(Loader<T> loader, T data) { Log.d("PageController", "Load finished; v=" + isUserVisible() + " o=" + isOpening()); if (isUserVisible() || !isOpening()) { Log.d("PageController", "Delivering result"); mResult = null; mLoaderCallbacks.onLoadFinished(loader, data); } else { Log.d("PageController", "Deferring result delivery"); mResult = new Pair<>(loader, data); mDeferredRunnables.add(this); } } protected boolean isOpening() { return PageController.this.mLoadsDeferred; } @Override public void onLoaderReset(Loader<T> loader) { mLoaderCallbacks.onLoaderReset(loader); mResult = null; } @Override public void run() { if (mResult != null) { Log.d("PageController", "Delivering deferred result"); mLoaderCallbacks.onLoadFinished(mResult.first, mResult.second); mResult = null; } } } }