Commit c42034e2 authored by Johan Olsson's avatar Johan Olsson Committed by luca020400

Jelly: Implementing favorite management through a ContentProvider.

Inspired by the history ContentProvider. Previously database
queries were being invoked on the UI thread. This is no longer
the case. Edit and delete favorite were also taken into account
and even the delete animation is working like before.

Edit, Add and Delete actions implemented as static inner
classes to avoid implicit activity reference. May otherwise
cause memory leakage.

Snackbar parent view is referenced as weak to avoid potential
illegal state exceptions.

Change-Id: I214ec25a90e86048da0e97be62f899d0af99c1a0
parent 3c456943
...@@ -96,6 +96,11 @@ ...@@ -96,6 +96,11 @@
android:resource="@xml/file_provider" /> android:resource="@xml/file_provider" />
</provider> </provider>
<provider
android:name=".favorite.FavoriteProvider"
android:authorities="org.lineageos.jelly.favorite"
android:exported="false" />
<provider <provider
android:name=".history.HistoryProvider" android:name=".history.HistoryProvider"
android:authorities="org.lineageos.jelly.history" android:authorities="org.lineageos.jelly.history"
......
...@@ -19,6 +19,7 @@ import android.Manifest; ...@@ -19,6 +19,7 @@ import android.Manifest;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.DownloadManager; import android.app.DownloadManager;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
...@@ -32,6 +33,7 @@ import android.graphics.drawable.Drawable; ...@@ -32,6 +33,7 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable; import android.graphics.drawable.TransitionDrawable;
import android.net.Uri; import android.net.Uri;
import android.net.http.HttpResponseCache; import android.net.http.HttpResponseCache;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.Handler; import android.os.Handler;
...@@ -70,9 +72,8 @@ import android.widget.LinearLayout; ...@@ -70,9 +72,8 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import org.lineageos.jelly.favorite.Favorite;
import org.lineageos.jelly.favorite.FavoriteActivity; import org.lineageos.jelly.favorite.FavoriteActivity;
import org.lineageos.jelly.favorite.FavoriteDatabaseHandler; import org.lineageos.jelly.favorite.FavoriteProvider;
import org.lineageos.jelly.history.HistoryActivity; import org.lineageos.jelly.history.HistoryActivity;
import org.lineageos.jelly.suggestions.SuggestionsAdapter; import org.lineageos.jelly.suggestions.SuggestionsAdapter;
import org.lineageos.jelly.ui.SearchBarController; import org.lineageos.jelly.ui.SearchBarController;
...@@ -86,6 +87,7 @@ import org.lineageos.jelly.webview.WebViewExtActivity; ...@@ -86,6 +87,7 @@ import org.lineageos.jelly.webview.WebViewExtActivity;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference;
public class MainActivity extends WebViewExtActivity implements public class MainActivity extends WebViewExtActivity implements
SearchBarController.OnCancelListener { SearchBarController.OnCancelListener {
...@@ -453,15 +455,12 @@ public class MainActivity extends WebViewExtActivity implements ...@@ -453,15 +455,12 @@ public class MainActivity extends WebViewExtActivity implements
} }
private void setAsFavorite(String title, String url) { private void setAsFavorite(String title, String url) {
FavoriteDatabaseHandler handler = new FavoriteDatabaseHandler(this);
boolean hasValidIcon = mUrlIcon != null && !mUrlIcon.isRecycled(); boolean hasValidIcon = mUrlIcon != null && !mUrlIcon.isRecycled();
int color = hasValidIcon ? UiUtils.getColor(mUrlIcon, false) : Color.TRANSPARENT; int color = hasValidIcon ? UiUtils.getColor(mUrlIcon, false) : Color.TRANSPARENT;
if (color == Color.TRANSPARENT) { if (color == Color.TRANSPARENT) {
color = ContextCompat.getColor(this, R.color.colorAccent); color = ContextCompat.getColor(this, R.color.colorAccent);
} }
handler.addItem(new Favorite(title, url, color)); new SetAsFavoriteTask(getContentResolver(), title, url, color, mCoordinator).execute();
Snackbar.make(mCoordinator, getString(R.string.favorite_added),
Snackbar.LENGTH_LONG).show();
} }
public void downloadFileAsk(String url, String contentDisposition, String mimeType) { public void downloadFileAsk(String url, String contentDisposition, String mimeType) {
...@@ -699,4 +698,36 @@ public class MainActivity extends WebViewExtActivity implements ...@@ -699,4 +698,36 @@ public class MainActivity extends WebViewExtActivity implements
super.onWindowFocusChanged(hasFocus); super.onWindowFocusChanged(hasFocus);
setImmersiveMode(hasFocus && mCustomView != null); setImmersiveMode(hasFocus && mCustomView != null);
} }
private static class SetAsFavoriteTask extends AsyncTask<Void, Void, Boolean> {
private ContentResolver contentResolver;
private final String title;
private final String url;
private final int color;
private final WeakReference<View> parentView;
SetAsFavoriteTask(ContentResolver contentResolver, String title, String url,
int color, View parentView) {
this.contentResolver = contentResolver;
this.title = title;
this.url = url;
this.color = color;
this.parentView = new WeakReference<>(parentView);
}
@Override
protected Boolean doInBackground(Void... params) {
FavoriteProvider.addOrUpdateItem(contentResolver, title, url, color);
return true;
}
@Override
protected void onPostExecute(Boolean aBoolean) {
View view = parentView.get();
if (view != null) {
Snackbar.make(view, view.getContext().getString(R.string.favorite_added),
Snackbar.LENGTH_LONG).show();
}
}
}
} }
/*
* Copyright (C) 2017 The LineageOS Project
*
* 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 org.lineageos.jelly.favorite;
public class Favorite {
private long id = -1;
private String title;
private String url;
private final int color;
public Favorite(String title, String url, int color) {
this.title = title;
this.url = url;
this.color = color;
}
Favorite(long id, String title, String url, int color) {
this.id = id;
this.title = title;
this.url = url;
this.color = color;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
String getUrl() {
return url;
}
void setUrl(String url) {
this.url = url;
}
public int getColor() {
return color;
}
}
...@@ -15,6 +15,14 @@ ...@@ -15,6 +15,14 @@
*/ */
package org.lineageos.jelly.favorite; package org.lineageos.jelly.favorite;
import android.app.LoaderManager;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
...@@ -32,14 +40,10 @@ import android.widget.LinearLayout; ...@@ -32,14 +40,10 @@ import android.widget.LinearLayout;
import org.lineageos.jelly.R; import org.lineageos.jelly.R;
import org.lineageos.jelly.utils.UiUtils; import org.lineageos.jelly.utils.UiUtils;
import java.util.ArrayList;
import java.util.List;
public class FavoriteActivity extends AppCompatActivity { public class FavoriteActivity extends AppCompatActivity {
private RecyclerView mList; private RecyclerView mList;
private View mEmptyView; private View mEmptyView;
private FavoriteDatabaseHandler mDbHandler;
private FavoriteAdapter mAdapter; private FavoriteAdapter mAdapter;
@Override @Override
...@@ -56,8 +60,31 @@ public class FavoriteActivity extends AppCompatActivity { ...@@ -56,8 +60,31 @@ public class FavoriteActivity extends AppCompatActivity {
mList = (RecyclerView) findViewById(R.id.favorite_list); mList = (RecyclerView) findViewById(R.id.favorite_list);
mEmptyView = findViewById(R.id.favorite_empty_layout); mEmptyView = findViewById(R.id.favorite_empty_layout);
mDbHandler = new FavoriteDatabaseHandler(this); mAdapter = new FavoriteAdapter(this);
mAdapter = new FavoriteAdapter(this, new ArrayList<>());
getLoaderManager().initLoader(0, null, new LoaderManager.LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(FavoriteActivity.this, FavoriteProvider.Columns.CONTENT_URI,
null, null, null, FavoriteProvider.Columns._ID + " DESC");
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
mAdapter.swapCursor(cursor);
if (cursor.getCount() == 0) {
mList.setVisibility(View.GONE);
mEmptyView.setVisibility(View.VISIBLE);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
});
mList.setLayoutManager(new GridLayoutManager(this, 2)); mList.setLayoutManager(new GridLayoutManager(this, 2));
mList.setItemAnimator(new DefaultItemAnimator()); mList.setItemAnimator(new DefaultItemAnimator());
mList.setAdapter(mAdapter); mList.setAdapter(mAdapter);
...@@ -76,30 +103,14 @@ public class FavoriteActivity extends AppCompatActivity { ...@@ -76,30 +103,14 @@ public class FavoriteActivity extends AppCompatActivity {
}); });
} }
@Override void editItem(long id, String title, String url) {
public void onResume() {
super.onResume();
refresh();
}
void refresh() {
List<Favorite> items = mDbHandler.getAllItems();
mAdapter.updateList(items);
if (items.isEmpty()) {
mList.setVisibility(View.GONE);
mEmptyView.setVisibility(View.VISIBLE);
}
}
void editItem(Favorite item) {
View view = LayoutInflater.from(this) View view = LayoutInflater.from(this)
.inflate(R.layout.dialog_favorite_edit, new LinearLayout(this)); .inflate(R.layout.dialog_favorite_edit, new LinearLayout(this));
EditText titleEdit = (EditText) view.findViewById(R.id.favorite_edit_title); EditText titleEdit = (EditText) view.findViewById(R.id.favorite_edit_title);
EditText urlEdit = (EditText) view.findViewById(R.id.favorite_edit_url); EditText urlEdit = (EditText) view.findViewById(R.id.favorite_edit_url);
titleEdit.setText(item.getTitle()); titleEdit.setText(title);
urlEdit.setText(item.getUrl()); urlEdit.setText(url);
String error = getString(R.string.favorite_edit_error); String error = getString(R.string.favorite_edit_error);
urlEdit.addTextChangedListener(new TextWatcher() { urlEdit.addTextChangedListener(new TextWatcher() {
...@@ -127,22 +138,19 @@ public class FavoriteActivity extends AppCompatActivity { ...@@ -127,22 +138,19 @@ public class FavoriteActivity extends AppCompatActivity {
.setView(view) .setView(view)
.setPositiveButton(R.string.favorite_edit_positive, .setPositiveButton(R.string.favorite_edit_positive,
((dialog, which) -> { ((dialog, which) -> {
String url = urlEdit.getText().toString(); String updatedUrl = urlEdit.getText().toString();
String title = titleEdit.getText().toString(); String updatedTitle = titleEdit.getText().toString();
if (url.isEmpty()) { if (url.isEmpty()) {
urlEdit.setError(error); urlEdit.setError(error);
urlEdit.requestFocus(); urlEdit.requestFocus();
} }
item.setTitle(title); new UpdateFavoriteTask(getContentResolver(), id, updatedTitle,
item.setUrl(url); updatedUrl).execute();
mDbHandler.updateItem(item);
refresh();
dialog.dismiss(); dialog.dismiss();
})) }))
.setNeutralButton(R.string.favorite_edit_delete, .setNeutralButton(R.string.favorite_edit_delete,
(dialog, which) -> { (dialog, which) -> {
mDbHandler.deleteItem(item.getId()); new DeleteFavoriteTask(getContentResolver()).execute(id);
mAdapter.removeItem(item.getId());
dialog.dismiss(); dialog.dismiss();
}) })
.setNegativeButton(android.R.string.cancel, .setNegativeButton(android.R.string.cancel,
...@@ -150,4 +158,40 @@ public class FavoriteActivity extends AppCompatActivity { ...@@ -150,4 +158,40 @@ public class FavoriteActivity extends AppCompatActivity {
.show(); .show();
} }
private static class UpdateFavoriteTask extends AsyncTask<Void, Void, Void> {
private final ContentResolver contentResolver;
private final long id;
private final String title;
private final String url;
UpdateFavoriteTask(ContentResolver contentResolver, long id, String title, String url) {
this.contentResolver = contentResolver;
this.id = id;
this.title = title;
this.url = url;
}
@Override
protected Void doInBackground(Void... params) {
FavoriteProvider.updateItem(contentResolver, id, title, url);
return null;
}
}
private static class DeleteFavoriteTask extends AsyncTask<Long, Void, Void> {
private final ContentResolver contentResolver;
DeleteFavoriteTask(ContentResolver contentResolver) {
this.contentResolver = contentResolver;
}
@Override
protected Void doInBackground(Long... ids) {
for (Long id : ids) {
Uri uri = ContentUris.withAppendedId(FavoriteProvider.Columns.CONTENT_URI, id);
contentResolver.delete(uri, null, null);
}
return null;
}
}
} }
...@@ -16,21 +16,42 @@ ...@@ -16,21 +16,42 @@
package org.lineageos.jelly.favorite; package org.lineageos.jelly.favorite;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.lineageos.jelly.R; import org.lineageos.jelly.R;
import java.util.List;
class FavoriteAdapter extends RecyclerView.Adapter<FavoriteHolder> { class FavoriteAdapter extends RecyclerView.Adapter<FavoriteHolder> {
private final Context mContext; private final Context mContext;
private List<Favorite> mList; private Cursor mCursor;
private int mIdColumnIndex;
private int mTitleColumnIndex;
private int mUrlColumnIndex;
private int mColorColumnIndex;
FavoriteAdapter(Context context, List<Favorite> list) { FavoriteAdapter(Context context) {
mContext = context; mContext = context;
mList = list; setHasStableIds(true);
}
void swapCursor(Cursor cursor) {
if (cursor == mCursor) {
return;
}
if (mCursor != null) {
mCursor.close();
}
mCursor = cursor;
if (mCursor != null) {
mIdColumnIndex = cursor.getColumnIndexOrThrow(FavoriteProvider.Columns._ID);
mTitleColumnIndex = cursor.getColumnIndexOrThrow(FavoriteProvider.Columns.TITLE);
mUrlColumnIndex = cursor.getColumnIndexOrThrow(FavoriteProvider.Columns.URL);
mColorColumnIndex = cursor.getColumnIndexOrThrow(FavoriteProvider.Columns.COLOR);
}
notifyDataSetChanged();
} }
@Override @Override
...@@ -41,37 +62,23 @@ class FavoriteAdapter extends RecyclerView.Adapter<FavoriteHolder> { ...@@ -41,37 +62,23 @@ class FavoriteAdapter extends RecyclerView.Adapter<FavoriteHolder> {
@Override @Override
public void onBindViewHolder(FavoriteHolder holder, int position) { public void onBindViewHolder(FavoriteHolder holder, int position) {
holder.setData(mContext, mList.get(position)); if (!mCursor.moveToPosition(position)) {
return;
}
long id = mCursor.getLong(mIdColumnIndex);
String title = mCursor.getString(mTitleColumnIndex);
String url = mCursor.getString(mUrlColumnIndex);
int color = mCursor.getInt(mColorColumnIndex);
holder.bind(mContext, id, title, url, color);
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
return mList.size(); return mCursor != null ? mCursor.getCount() : 0;
} }
void updateList(List<Favorite> list) { @Override
mList = list; public long getItemId(int position) {
notifyDataSetChanged(); return mCursor.moveToPosition(position) ? mCursor.getLong(mIdColumnIndex) : -1;
}
void removeItem(long id) {
int position = 0;
for (; position < mList.size(); position++) {
if (mList.get(position).getId() == id) {
break;
}
}
if (position == mList.size()) {
return;
}
mList.remove(position);
notifyItemRemoved(position);
if (mList.isEmpty()) {
// Show empty status
((FavoriteActivity) mContext).refresh();
}
} }
} }
/*
* Copyright (C) 2017 The LineageOS Project
*
* 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 org.lineageos.jelly.favorite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.util.ArrayList;
import java.util.List;
public class FavoriteDatabaseHandler extends SQLiteOpenHelper {
private static final int DB_VERSION = 1;
private static final String DB_NAME = "FavoriteDatabase";
private static final String DB_TABLE_FAVORITES = "favorites";
private static final String KEY_ID = "id";
private static final String KEY_TITLE = "title";
private static final String KEY_URL = "url";
private static final String KEY_COLOR = "color";
public FavoriteDatabaseHandler(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + DB_TABLE_FAVORITES + " (" +
KEY_ID + " INTEGER PRIMARY KEY, " +
KEY_TITLE + " TEXT, " +
KEY_URL + " TEXT, " +
KEY_COLOR + " INTEGER)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Update this when db table will be changed
}
public void addItem(Favorite item) {
if (item.getId() == -1) {
item.setId(System.currentTimeMillis());
}
ContentValues values = new ContentValues();
values.put(KEY_ID, item.getId());
values.put(KEY_TITLE, item.getTitle());
values.put(KEY_URL, item.getUrl());
values.put(KEY_COLOR, item.getColor());
SQLiteDatabase db = getWritableDatabase();
db.insert(DB_TABLE_FAVORITES, null, values);
db.close();
}
void updateItem(Favorite item) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(KEY_TITLE, item.getTitle());
values.put(KEY_URL, item.getUrl());
values.put(KEY_COLOR, item.getColor());
db.update(DB_TABLE_FAVORITES, values, KEY_ID + "=?",
new String[]{String.valueOf(item.getId())});
}
void deleteItem(long id) {
SQLiteDatabase db = getWritableDatabase();
db.delete(DB_TABLE_FAVORITES, KEY_ID + "=?", new String[]{String.valueOf(id)});
db.close();
}
List<Favorite> getAllItems() {
List<Favorite> list = new ArrayList<>();
SQLiteDatabase db = getWritableDatabase();
Cursor cursor = db.rawQuery("SELECT * FROM " + DB_TABLE_FAVORITES +
" ORDER BY " + KEY_ID + " DESC", null);
if (cursor.moveToFirst()) {
do {
list.add(new Favorite(Long.parseLong(cursor.getString(0)),
cursor.getString(1), cursor.getString(2),
Integer.parseInt(cursor.getString(3))));
} while (cursor.moveToNext());
}
cursor.close();
db.close();
return list;
}
}
...@@ -38,23 +38,20 @@ class FavoriteHolder extends RecyclerView.ViewHolder { ...@@ -38,23 +38,20 @@ class FavoriteHolder extends RecyclerView.ViewHolder {
mTitle = (TextView) view.findViewById(R.id.row_favorite_title); mTitle = (TextView) view.findViewById(R.id.row_favorite_title);
} }
void setData(Context context, Favorite item) { void bind(Context context, long id, String title, String url, int color) {
String title = item.getTitle(); String adjustedTitle = title == null || title.isEmpty() ? url.split("/")[2] : title;
if (title == null || title.isEmpty()) { mTitle.setText(adjustedTitle);
title = item.getUrl().split("/")[2]; mTitle.setTextColor(UiUtils.isColorLight(color) ? Color.BLACK : Color.WHITE);
} mCard.setCardBackgroundColor(color);
mTitle.setText(title);
mTitle.setTextColor(UiUtils.isColorLight(item.getColor()) ? Color.BLACK : Color.WHITE);
mCard.setCardBackgroundColor(item.getColor());
mCard.setOnClickListener(v -> { mCard.setOnClickListener(v -> {
Intent intent = new Intent(context, MainActivity.class); Intent intent = new Intent(context, MainActivity.class);
intent.setData(Uri.parse(item.getUrl())); intent.setData(Uri.parse(url));
context.startActivity(intent); context.startActivity(intent);
}); });
mCard.setOnLongClickListener(v -> { mCard.setOnLongClickListener(v -> {
((FavoriteActivity) context).editItem(item); ((FavoriteActivity) context).editItem(id, adjustedTitle, url);
return true; return true;
}); });
} }
......
/*
* Copyright (C) 2017 The LineageOS Project
*
* 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 org.lineageos.jelly.favorite;