/*
* 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.app.Fragment;
import android.app.LoaderManager;
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.Loader;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.util.SimpleArrayMap;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.TextView;
import com.appsimobile.appsii.R;
import com.appsimobile.appsii.compat.LauncherAppsCompat;
import com.appsimobile.appsii.dagger.AppInjector;
import com.appsimobile.appsii.module.BaseListAdapter;
import com.appsimobile.appsii.module.ViewHolder;
import com.mobeta.android.dslv.ConditionalRemovableAdapter;
import com.mobeta.android.dslv.DragSortListView;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
/**
* Created by nick on 31/08/14.
*/
public class ReorderAppsFragment extends Fragment implements DragSortListView.RemoveListener,
DragSortListView.DropListener, AdapterView.OnItemClickListener {
/**
* The list-view used to re-order the tags. We register a few listeners on the
* list to get updates when items are re-ordered or removed
*/
DragSortListView mDragSortListView;
/**
* The adapter applied to the list-view.
*/
AppsAdapter mAppsAdapter;
/**
* True when we are updating the database. While the database is being updated,
* we ignore changes to the tags and don't update the underlying list to prevent
* flickering.
*/
boolean mIsChangeInProgress;
/**
* The handler used to apply the user's changes
*/
@Nullable
QueryHandler mQueryHandler;
RetainQueryHelperFragment mRetainFragment;
@Inject
LauncherAppsCompat mLauncherAppsCompat;
private AppTag mAppTag;
public static Fragment createInstance(AppTag appTag) {
ReorderAppsFragment result = new ReorderAppsFragment();
Bundle args = new Bundle();
args.putParcelable("tag", appTag);
result.setArguments(args);
return result;
}
@Override
public void onStart() {
super.onStart();
mRetainFragment.setAdapter(mAppsAdapter);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppInjector.inject(this);
// create the adapter here to make sure we can set it's data and register it's listener
// This ensures the life cycles are the same
mAppsAdapter = new AppsAdapter();
// now get the tag we need to display the data from
if (savedInstanceState != null) {
mAppTag = savedInstanceState.getParcelable("tag");
}
// see if the retain fragment is already present. Otherwise create it.
// the key has to be unique because multiple pages may be added by the pager.
String key = "retain";
mRetainFragment =
(RetainQueryHelperFragment) getFragmentManager().findFragmentByTag(key);
// when the fragment was null, create it and add it.
if (mRetainFragment == null) {
mRetainFragment = new RetainQueryHelperFragment();
getFragmentManager().beginTransaction().add(mRetainFragment, key).commit();
}
// now that the fragment is added, initialize the loader.
getLoaderManager().initLoader(100, null, new AllAppsLoaderCallbacks());
if (mAppTag != null) {
mRetainFragment.loadTaggedApps(mAppTag.id);
}
}
@Override
public void onStop() {
super.onStop();
mRetainFragment.setAdapter(null);
}
@Override
public void remove(int i) {
mIsChangeInProgress = true;
TaggedApp item = mAppsAdapter.getItem(i);
doRemoveTaggedApp(item);
// set this after removal. Otherwise the query-handler does
// not know if it is a newly deleted item or not.
// The queryHandler uses the mDeleted field to know if it is
// a second delete
if (item.mDeleted) {
mAppsAdapter.removeItem(i);
} else {
item.mDeleted = true;
mAppsAdapter.notifyDataSetChanged();
}
}
void doRemoveTaggedApp(TaggedApp item) {
// We only create the query-handler if it is really needed. So create
// it when null
if (mQueryHandler == null) {
mQueryHandler = new QueryHandler(getActivity().getContentResolver());
}
mQueryHandler.removeTaggedApp(item);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable("tag", mAppTag);
}
@Override
public void drop(int from, int to) {
if (from != to) {
mIsChangeInProgress = true;
doUpdateOrder(mAppsAdapter.getItems(), from, to);
}
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_reorder_app_tags, container, false);
}
/**
* Starts an update in the order. The order in the database is changed to
* reflect the order in the given list with the item at position from
* changed to 'to'.
*/
private void doUpdateOrder(List<TaggedApp> items, int from, int to) {
// We only create the query-handler if it is really needed. So create
// it when null
if (mQueryHandler == null) {
mQueryHandler = new QueryHandler(getActivity().getContentResolver());
}
// create a new list, make this list the same size as the original one,
// and apply the change to it.
int count = items.size();
List<TaggedApp> newOrdering = new ArrayList<>(count);
newOrdering.addAll(items);
TaggedApp moved = newOrdering.remove(from);
newOrdering.add(to, moved);
// now update the adapter to make sure it already reflects the change
mAppsAdapter.setItems(newOrdering);
// start the update on the database
mQueryHandler.updateOrdering(newOrdering);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mDragSortListView = (DragSortListView) view.findViewById(R.id.sort_list_view);
mDragSortListView.setDropListener(this);
mDragSortListView.setRemoveListener(this);
mDragSortListView.setAdapter(mAppsAdapter);
mDragSortListView.setOnItemClickListener(this);
}
public void setAppTag(AppTag appTag) {
mAppTag = appTag;
mRetainFragment.loadTaggedApps(appTag.id);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
TaggedApp app = mAppsAdapter.getItem(position);
if (app.mDeleted) {
if (mQueryHandler == null) {
mQueryHandler = new QueryHandler(getActivity().getContentResolver());
}
mQueryHandler.undeleteTaggedApp(app);
app.mDeleted = false;
mAppsAdapter.notifyDataSetChanged();
}
}
void onDeleteFinished() {
mIsChangeInProgress = false;
}
void onUpdateFinished() {
mIsChangeInProgress = false;
}
void onAllAppsLoaded(List<AppEntry> entries) {
mRetainFragment.setAllApps(entries);
}
public static class TaggedAppViewHolder extends ViewHolder {
final TextView mTextView;
final View mDragHandle;
final View mUndoDelete;
final Context mContext;
AppIconLoaderTaskImpl mAppIconLoaderTask;
public TaggedAppViewHolder(View view) {
super(view);
mContext = view.getContext();
mTextView = (TextView) view.findViewById(R.id.tag_title);
mDragHandle = view.findViewById(R.id.drag_handle);
mUndoDelete = view.findViewById(R.id.undo_delete_text);
}
public void loadIcon(Context context, TaggedApp item) {
if (mAppIconLoaderTask != null) {
mAppIconLoaderTask.cancel(true);
}
mAppIconLoaderTask =
new AppIconLoaderTaskImpl(mContext, item.mAppEntry,
context.getPackageManager());
mAppIconLoaderTask.execute();
}
/**
* When true, this item was just removed. We display an undo text
* to restore the view
*/
void setLeaveBehindVisible(boolean visible) {
if (visible) {
mUndoDelete.setVisibility(View.VISIBLE);
mTextView.setVisibility(View.GONE);
mDragHandle.setVisibility(View.GONE);
} else {
mUndoDelete.setVisibility(View.GONE);
mTextView.setVisibility(View.VISIBLE);
mDragHandle.setVisibility(View.VISIBLE);
}
}
void onIconLoaded(Drawable drawable) {
int dimen = (int) (mContext.getResources().getDisplayMetrics().density * 42);
drawable.setBounds(0, 0, dimen, dimen);
mTextView.setCompoundDrawables(drawable, null, null, null);
}
class AppIconLoaderTaskImpl extends AppIconLoaderTask {
AppIconLoaderTaskImpl(Context context, AppEntry appEntry,
PackageManager packageManager) {
super(context, appEntry, packageManager);
}
@Override
protected void onPostExecute(Drawable drawable) {
onIconLoaded(drawable);
}
}
}
/**
* A simple fragment that will retain some of the loading state to make it quicker to
* restore the state of the adapter
*/
public static class RetainQueryHelperFragment extends Fragment {
final SimpleArrayMap<ComponentName, AppEntry> mAppsByComponent = new SimpleArrayMap<>();
List<AppEntry> mAllApps;
List<TaggedApp> mTaggedApps;
long mAppTagId = Long.MIN_VALUE;
QueryHandler mQueryHandler;
AppsAdapter mAppsAdapter;
boolean mLoadRequested;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
mQueryHandler = new QueryHandler(getActivity().getContentResolver());
}
public void setAdapter(AppsAdapter adapter) {
mAppsAdapter = adapter;
if (adapter != null) {
if (mTaggedApps != null) {
mAppsAdapter.setItems(mTaggedApps);
} else {
mAppsAdapter.clear();
}
}
}
public void setAllApps(List<AppEntry> allApps) {
mAllApps = allApps;
mAppsByComponent.clear();
int size = allApps.size();
for (int i = 0; i < size; i++) {
AppEntry e = allApps.get(i);
mAppsByComponent.put(e.getComponentName(), e);
}
loadIfNeeded();
}
private void loadIfNeeded() {
if (mLoadRequested) {
if (mAllApps != null) {
mQueryHandler.loadTaggedApps();
mLoadRequested = false;
}
}
}
void loadTaggedApps(long tagId) {
if (mAppTagId != tagId) {
mAppTagId = tagId;
if (mAllApps == null) {
mLoadRequested = true;
} else {
mQueryHandler.loadTaggedApps();
}
}
}
void setAppsInTag(List<TaggedApp> result) {
mTaggedApps = result;
if (mAppsAdapter != null) {
mAppsAdapter.setItems(result);
}
}
class QueryHandler extends AsyncQueryHandler {
int mActiveToken = 0;
public QueryHandler(ContentResolver cr) {
super(cr);
}
void loadTaggedApps() {
cancelOperation(mActiveToken);
mActiveToken++;
startQuery(mActiveToken, null,
ContentUris.withAppendedId(
AppsContract.TaggedAppColumns.CONTENT_URI, mAppTagId),
AppQuery.PROJECTION,
null, null, AppQuery.ORDER);
}
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
// only handle the result when the token was not changed
if (token == mActiveToken) {
List<TaggedApp> result = new ArrayList<>();
while (cursor.moveToNext()) {
String comp = cursor.getString(AppQuery.COMPONENT_NAME);
ComponentName componentName = ComponentName.unflattenFromString(comp);
AppEntry appEntry = mAppsByComponent.get(componentName);
if (appEntry == null) continue;
TaggedApp app = new TaggedApp();
app.mId = cursor.getLong(AppQuery._ID);
app.mTagId = cursor.getLong(AppQuery.TAG_ID);
app.mTagName = cursor.getString(AppQuery.TAG_NAME);
app.mAppEntry = appEntry;
app.mComponentName = componentName;
result.add(app);
}
setAppsInTag(result);
}
}
}
}
public class AppsAdapter extends BaseListAdapter<TaggedApp, TaggedAppViewHolder> implements
ConditionalRemovableAdapter {
@Override
protected long getItemId(TaggedApp appTag) {
return appTag.mId;
}
@Override
protected TaggedAppViewHolder newViewHolder(LayoutInflater inflater, ViewGroup parent) {
View view = inflater.inflate(R.layout.list_item_tagged_app, parent, false);
return new TaggedAppViewHolder(view);
}
@Override
protected void bindViewHolder(TaggedApp item, TaggedAppViewHolder holder) {
holder.mTextView.setText(item.mAppEntry.getLabel());
holder.loadIcon(holder.mTextView.getContext(), item);
holder.setLeaveBehindVisible(item.mDeleted);
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean isEnabled(int position) {
// when the item is deleted, clicking it is enabled
return getItem(position).mDeleted;
}
@Override
public boolean canRemove(int pos) {
return true;
}
}
public class QueryHandler extends AsyncQueryHandler {
private int mUpdatingCount;
public QueryHandler(ContentResolver cr) {
super(cr);
}
public void updateOrdering(List<TaggedApp> newOrdering) {
int size = newOrdering.size();
mUpdatingCount = size;
for (int i = 0; i < size; i++) {
ContentValues values = new ContentValues();
values.put(AppsContract.TagColumns.POSITION, i);
TaggedApp tag = newOrdering.get(i);
Uri uri = ContentUris.withAppendedId(AppsContract.TaggedAppColumns.CONTENT_URI,
tag.mId);
startUpdate(i, tag, uri, values, null, null);
}
}
public void undeleteTaggedApp(TaggedApp app) {
ContentValues contentValues = new ContentValues();
contentValues.put(AppsContract.TaggedAppColumns.DELETED, 0);
startUpdate(2, app,
ContentUris.withAppendedId(AppsContract.TaggedAppColumns.CONTENT_URI, app.mId),
contentValues,
null,
null);
}
@Override
protected void onUpdateComplete(int token, Object cookie, int result) {
if (token == 0) {
mUpdatingCount--;
if (mUpdatingCount == 0) {
onUpdateFinished();
}
} else if (token == 1) {
onDeleteFinished();
} else if (token == 2) {
// undelete finished
}
}
public void removeTaggedApp(TaggedApp app) {
startDelete(0, null,
AppsContract.TaggedAppColumns.CONTENT_URI, AppQuery.WHERE_DELETED, null);
if (app.mDeleted) return;
ContentValues contentValues = new ContentValues();
contentValues.put(AppsContract.TaggedAppColumns.DELETED, 1);
startUpdate(1, app,
ContentUris.withAppendedId(AppsContract.TaggedAppColumns.CONTENT_URI, app.mId),
contentValues,
null,
null);
}
}
class AllAppsLoaderCallbacks implements LoaderManager.LoaderCallbacks<List<AppEntry>> {
@Override
public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) {
return new AppListLoader(getActivity(), mLauncherAppsCompat);
}
@Override
public void onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> data) {
onAllAppsLoaded(data);
}
@Override
public void onLoaderReset(Loader<List<AppEntry>> loader) {
}
}
}