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 @@
android:resource="@xml/file_provider" />
</provider>
<provider
android:name=".favorite.FavoriteProvider"
android:authorities="org.lineageos.jelly.favorite"
android:exported="false" />
<provider
android:name=".history.HistoryProvider"
android:authorities="org.lineageos.jelly.history"
......
......@@ -19,6 +19,7 @@ import android.Manifest;
import android.app.ActivityManager;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
......@@ -32,6 +33,7 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.net.Uri;
import android.net.http.HttpResponseCache;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
......@@ -70,9 +72,8 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.lineageos.jelly.favorite.Favorite;
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.suggestions.SuggestionsAdapter;
import org.lineageos.jelly.ui.SearchBarController;
......@@ -86,6 +87,7 @@ import org.lineageos.jelly.webview.WebViewExtActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
public class MainActivity extends WebViewExtActivity implements
SearchBarController.OnCancelListener {
......@@ -453,15 +455,12 @@ public class MainActivity extends WebViewExtActivity implements
}
private void setAsFavorite(String title, String url) {
FavoriteDatabaseHandler handler = new FavoriteDatabaseHandler(this);
boolean hasValidIcon = mUrlIcon != null && !mUrlIcon.isRecycled();
int color = hasValidIcon ? UiUtils.getColor(mUrlIcon, false) : Color.TRANSPARENT;
if (color == Color.TRANSPARENT) {
color = ContextCompat.getColor(this, R.color.colorAccent);
}
handler.addItem(new Favorite(title, url, color));
Snackbar.make(mCoordinator, getString(R.string.favorite_added),
Snackbar.LENGTH_LONG).show();
new SetAsFavoriteTask(getContentResolver(), title, url, color, mCoordinator).execute();
}
public void downloadFileAsk(String url, String contentDisposition, String mimeType) {
......@@ -699,4 +698,36 @@ public class MainActivity extends WebViewExtActivity implements
super.onWindowFocusChanged(hasFocus);
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 @@
*/
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.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
......@@ -32,14 +40,10 @@ import android.widget.LinearLayout;
import org.lineageos.jelly.R;
import org.lineageos.jelly.utils.UiUtils;
import java.util.ArrayList;
import java.util.List;
public class FavoriteActivity extends AppCompatActivity {
private RecyclerView mList;
private View mEmptyView;
private FavoriteDatabaseHandler mDbHandler;
private FavoriteAdapter mAdapter;
@Override
......@@ -56,8 +60,31 @@ public class FavoriteActivity extends AppCompatActivity {
mList = (RecyclerView) findViewById(R.id.favorite_list);
mEmptyView = findViewById(R.id.favorite_empty_layout);
mDbHandler = new FavoriteDatabaseHandler(this);
mAdapter = new FavoriteAdapter(this, new ArrayList<>());
mAdapter = new FavoriteAdapter(this);
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.setItemAnimator(new DefaultItemAnimator());
mList.setAdapter(mAdapter);
......@@ -76,30 +103,14 @@ public class FavoriteActivity extends AppCompatActivity {
});
}
@Override
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) {
void editItem(long id, String title, String url) {
View view = LayoutInflater.from(this)
.inflate(R.layout.dialog_favorite_edit, new LinearLayout(this));
EditText titleEdit = (EditText) view.findViewById(R.id.favorite_edit_title);
EditText urlEdit = (EditText) view.findViewById(R.id.favorite_edit_url);
titleEdit.setText(item.getTitle());
urlEdit.setText(item.getUrl());
titleEdit.setText(title);
urlEdit.setText(url);
String error = getString(R.string.favorite_edit_error);
urlEdit.addTextChangedListener(new TextWatcher() {
......@@ -127,22 +138,19 @@ public class FavoriteActivity extends AppCompatActivity {
.setView(view)
.setPositiveButton(R.string.favorite_edit_positive,
((dialog, which) -> {
String url = urlEdit.getText().toString();
String title = titleEdit.getText().toString();
String updatedUrl = urlEdit.getText().toString();
String updatedTitle = titleEdit.getText().toString();
if (url.isEmpty()) {
urlEdit.setError(error);
urlEdit.requestFocus();
}
item.setTitle(title);
item.setUrl(url);
mDbHandler.updateItem(item);
refresh();
new UpdateFavoriteTask(getContentResolver(), id, updatedTitle,
updatedUrl).execute();
dialog.dismiss();
}))
.setNeutralButton(R.string.favorite_edit_delete,
(dialog, which) -> {
mDbHandler.deleteItem(item.getId());
mAdapter.removeItem(item.getId());
new DeleteFavoriteTask(getContentResolver()).execute(id);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel,
......@@ -150,4 +158,40 @@ public class FavoriteActivity extends AppCompatActivity {
.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 @@
package org.lineageos.jelly.favorite;
import android.content.Context;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import org.lineageos.jelly.R;
import java.util.List;
class FavoriteAdapter extends RecyclerView.Adapter<FavoriteHolder> {
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;
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
......@@ -41,37 +62,23 @@ class FavoriteAdapter extends RecyclerView.Adapter<FavoriteHolder> {
@Override
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
public int getItemCount() {
return mList.size();
return mCursor != null ? mCursor.getCount() : 0;
}
void updateList(List<Favorite> list) {
mList = list;
notifyDataSetChanged();
}
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();
}
@Override
public long getItemId(int position) {
return mCursor.moveToPosition(position) ? mCursor.getLong(mIdColumnIndex) : -1;
}
}
/*
* 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 {
mTitle = (TextView) view.findViewById(R.id.row_favorite_title);
}
void setData(Context context, Favorite item) {
String title = item.getTitle();
if (title == null || title.isEmpty()) {
title = item.getUrl().split("/")[2];
}
mTitle.setText(title);
mTitle.setTextColor(UiUtils.isColorLight(item.getColor()) ? Color.BLACK : Color.WHITE);
mCard.setCardBackgroundColor(item.getColor());
void bind(Context context, long id, String title, String url, int color) {
String adjustedTitle = title == null || title.isEmpty() ? url.split("/")[2] : title;
mTitle.setText(adjustedTitle);
mTitle.setTextColor(UiUtils.isColorLight(color) ? Color.BLACK : Color.WHITE);
mCard.setCardBackgroundColor(color);
mCard.setOnClickListener(v -> {
Intent intent = new Intent(context, MainActivity.class);
intent.setData(Uri.parse(item.getUrl()));
intent.setData(Uri.parse(url));
context.startActivity(intent);
});
mCard.setOnLongClickListener(v -> {
((FavoriteActivity) context).editItem(item);
((FavoriteActivity) context).editItem(id, adjustedTitle, url);
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;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.provider.BaseColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public class FavoriteProvider extends ContentProvider {
public interface Columns extends BaseColumns {
String AUTHORITY = "org.lineageos.jelly.favorite";
Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/favorite");
String TITLE = "title";
String URL = "url";
String COLOR = "color";
}
private static final int MATCH_ALL = 0;
private static final int MATCH_ID = 1;
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(Columns.AUTHORITY, "favorite", MATCH_ALL);
sURIMatcher.addURI(Columns.AUTHORITY, "favorite/#", MATCH_ID);
}
private FavoriteDbHelper mDbHelper;
public static void addOrUpdateItem(ContentResolver resolver, String title, String url,
int color) {
long existingId = -1;
Cursor cursor = resolver.query(Columns.CONTENT_URI, new String[] { Columns._ID },
Columns.URL + "=?", new String[] { url }, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
existingId = cursor.getLong(0);
}
cursor.close();
}
ContentValues values = new ContentValues();
values.put(Columns.TITLE, title);
values.put(Columns.COLOR, color);
if (existingId >= 0) {
resolver.update(ContentUris.withAppendedId(Columns.CONTENT_URI, existingId),
values, null, null);
} else {
values.put(Columns.URL, url);
resolver.insert(Columns.CONTENT_URI, values);
}
}
public static void updateItem(ContentResolver resolver, long id, String title, String url) {
ContentValues values = new ContentValues();
values.put(Columns.TITLE, title);
values.put(Columns.URL, url);
resolver.update(ContentUris.withAppendedId(Columns.CONTENT_URI, id), values, null, null);
}
@Override
public boolean onCreate() {
mDbHelper = new FavoriteDbHelper(getContext());
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection,
@Nullable String selection, @Nullable String[] selectionArgs,
@Nullable String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
int match = sURIMatcher.match(uri);
qb.setTables(FavoriteDbHelper.DB_TABLE_FAVORITES);
switch (match) {
case MATCH_ALL:
break;
case MATCH_ID:
qb.appendWhere(Columns._ID + " = " + uri.getLastPathSegment());
break;
default:
return null;
}
SQLiteDatabase db = mDbHelper.getReadableDatabase();
Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
ret.setNotificationUri(getContext().getContentResolver(), uri);